diff --git a/House.py b/House.py new file mode 100644 index 0000000..68f4bb7 --- /dev/null +++ b/House.py @@ -0,0 +1,925 @@ + +from time import sleep +from gdpc import Editor, Block, geometry +import numpy as np +import math +import matplotlib.pyplot as plt + +class House: + def __init__(self, editor, coordinates_min, coordinates_max, direction, list_block): + self.editor = editor + self.coordinates_min = coordinates_min + self.coordinates_max = coordinates_max + self.skeleton = [] + + size = [(coordinates_max[i] - coordinates_min[i]) + 10 for i in range(3)] + + self.grid3d = np.zeros(size, dtype=[('bool', bool), ('int', int)]) + + self.nbEtage = (coordinates_max[1] - coordinates_min[1]) // 5 + + self.direction = direction + + self.entranceWall = None + + self.blocks = list_block + + self.entranceCo = None + + + self.wall = Block(list_block["wall"]) + self.roof = Block(list_block["roof"]) + self.roof_slab = Block(list_block["roof_slab"]) + self.door = Block(list_block["door"]) + self.window = Block(list_block["window"]) + self.entrance = Block(list_block["entrance"]) + self.stairs = Block(list_block["stairs"]) + self.celling = Block(list_block["celling"]) + self.floor = Block(list_block["floor"]) + self.celling_slab = Block(list_block["celling_slab"]) + self.gardenOutline = Block(list_block["garden_outline"]) + self.garden_floor = Block(list_block["garden_floor"]) + + + + def createHouseSkeleton(self): + self.delete() + x_min, y_min, z_min = self.coordinates_min + x_max, y_max, z_max = self.coordinates_max + + perimeter_width = x_max - x_min + perimeter_depth = z_max - z_min + + x_min += 1 + z_min += 1 + x_max -= 1 + z_max -= 1 + x = np.random.randint(x_min+1 , x_max-1) + z = np.random.randint(z_min+1 , z_max-1 ) + + width = perimeter_width // 2 + depth = perimeter_depth // 2 + height = y_max - y_min + + if x + width-1 > x_max-1: + x = x_max - width-1 + if z + depth-1 > z_max-1: + z = z_max - depth-1 + + x_plan3d = x - x_min + z_plan3d = z - z_min + + for i in range(0, width-1): + for j in range(0, depth-1): + self.editor.placeBlock((x + i, y_min, z + j), self.floor) + self.grid3d[x_plan3d+i,0,z_plan3d+j] = True,1 + self.skeleton.append((x, z, width-1, depth-1, height)) + print("Coordinates of the corners: ", (x, z), (x, z+depth-1), (x+width-1, z), (x+width-1, z+depth-1)) + + + + + x_min -= 1 + x_max -= 1 + z_min += 1 + z_max += 1 + + for _ in range(3): + print("Rectangle n°", _+1, "en cours de création") + + + for a in range(100000): + new_width = np.random.randint(5, width-2) + new_depth = np.random.randint(5, depth-2) + + new_x = np.random.randint(max(x_min+1, x - new_width ), min(x_max-new_width - 1, x + width )) + new_z = np.random.randint(max(z_min+1, z - new_depth), min(z_max-new_depth - 1, z + depth )) + + new_x_plan3d = new_x - x_min -1 + new_z_plan3d = new_z - z_min +1 + + adjacent_blocks = 0 + for i in range(new_x_plan3d, new_x_plan3d + new_width): + for j in range(new_z_plan3d, new_z_plan3d + new_depth): + if self.grid3d[i-1,0,j]['bool'] and self.grid3d[i-1,0,j]['int']==1 or self.grid3d[i+1,0,j]['bool'] and self.grid3d[i+1,0,j]['int']==1 or self.grid3d[i,0,j-1]['bool'] and self.grid3d[i,0,j-1]['int']==1 or self.grid3d[i,0,j+1]['bool'] and self.grid3d[i,0,j+1]['int']==1: + adjacent_blocks += 1 + + if adjacent_blocks < 3: + continue + + if not np.any(self.grid3d[new_x_plan3d:new_x_plan3d+new_width,0, new_z_plan3d:new_z_plan3d+new_depth]['bool']): + new_x_plan3d = new_x - x_min + new_z_plan3d = new_z - z_min + for i in range(0, new_width): + for j in range(0, new_depth): + self.grid3d[new_x_plan3d + i,0, new_z_plan3d + j] = True,2 + + if i == 0 or i == new_width-1 or j == 0 or j == new_depth-1: + continue + else: + self.editor.placeBlock((new_x + i, y_min, new_z + j), self.floor) + + self.skeleton.append((new_x, new_z, new_width, new_depth, height)) + break + else: + print("Failed to place rectangle after 1000 attempts.") + + + def delete(self): + for x in range(self.coordinates_min[0], self.coordinates_max[0]): + for y in range(self.coordinates_min[1], self.coordinates_max[1]+10): + for z in range(self.coordinates_min[2], self.coordinates_max[2]): + self.editor.placeBlock((x, y, z), Block("air")) + + def putWallOnSkeleton(self): + for k in range(len(self.skeleton)): + x, z, width, depth, height = self.skeleton[k] + + + if k!= 0: + x+=1 + z+=1 + width-=2 + depth-=2 + x_plan3d = x - self.coordinates_min[0] + z_plan3d = z - self.coordinates_min[2] + for i in range(-1, width+1): + for j in range(-1, depth+1): + for y in range(0, height): + if i == -1 or i == width or j == -1 or j == depth: + if not (self.grid3d[x_plan3d + i,y, z_plan3d + j]['bool']) and not (self.grid3d[x_plan3d + i,y, z_plan3d + j]['int'] == 1) or (self.grid3d[x_plan3d + i,y, z_plan3d + j]['bool'] and self.grid3d[x_plan3d + i,y, z_plan3d + j]['int'] == 2) or y==0: + self.editor.placeBlock((x + i, self.coordinates_min[1] + y, z + j), self.wall) + self.grid3d[ x_plan3d+i, y, z_plan3d+j] = True + + + def getAdjacentWalls(self): + + main_rect = self.skeleton[0] + x_main, z_main, width_main, depth_main, heigt_main = main_rect + adjacent_walls = [] + width_main-=1 + depth_main-=1 + + for k in range(1, len(self.skeleton)): + x, z, width, depth, heigt = self.skeleton[k] + + + walls = [(x, z, x + width-1, z), (x, z, x, z + depth-1), (x, z + depth-1, x + width-1, z + depth-1), (x + width-1, z, x + width-1, z + depth-1)] + for wall in walls: + x1, z1, x2, z2 = wall + if (x_main <= x1 <= x_main + width_main or x_main <= x2 <= x_main + width_main) and (z_main - 1 == z1 or z_main + depth_main + 1 == z1): + x1 = max(x1, x_main-1) + x2 = min(x2, x_main + width_main+1) + if abs(x2 - x1) > 1: + adjacent_walls.append((x1, z1, x2, z2)) + elif (z_main <= z1 <= z_main + depth_main or z_main <= z2 <= z_main + depth_main) and (x_main - 1 == x1 or x_main + width_main + 1 == x1): + z1 = max(z1, z_main-1) + z2 = min(z2, z_main + depth_main+1) + if abs(z2 - z1) > 1: + adjacent_walls.append((x1, z1, x2, z2)) + + return adjacent_walls + + + + + def placeDoor(self): + walls = self.getAdjacentWalls() + for wall in walls: + for i in range(self.nbEtage): + x_min, z_min, x_max, z_max = wall + if x_min == x_max: + width = z_max - z_min + if width % 2 != 0: + door_pos = width // 2 + for y in range(self.coordinates_min[1]+1+i*4, self.coordinates_min[1]+3+i*4): + self.editor.placeBlock((x_min, y, z_min + door_pos), Block("air")) + self.editor.placeBlock((x_min, y, z_min + door_pos+1), Block("air")) + else: + door_pos = width // 2 + for y in range(self.coordinates_min[1]+1+i*4 , self.coordinates_min[1]+3+i*4): + self.editor.placeBlock((x_min, y, z_min + door_pos), Block("air")) + else: + width = x_max - x_min + if width % 2 != 0: + door_pos = width // 2 + for y in range(self.coordinates_min[1]+1+i*4, self.coordinates_min[1]+3+i*4): + self.editor.placeBlock((x_min + door_pos, y, z_min), Block("air")) + self.editor.placeBlock((x_min + door_pos+1, y, z_min), Block("air")) + + else: + door_pos = width // 2 + for y in range(self.coordinates_min[1]+1+i*4, self.coordinates_min[1]+3+i*4): + self.editor.placeBlock((x_min + door_pos, y, z_min), Block("air")) + + def placeRoof(self): + for k in range(len(self.skeleton)-1, -1, -1): + x, z, width, depth, height = self.skeleton[k] + + + + if k!= 0: + x+=1 + z+=1 + width-=2 + depth-=2 + if width < depth: + if width <=5: + n = 1 + elif width <=10: + n = 2 + else: + n = 3 + else: + if depth <=5: + n = 1 + elif depth <=10: + n = 2 + else: + n = 3 + else: + + if width < depth: + n = width // 4 + else: + n = depth // 4 + + + x_plan3d = x - self.coordinates_min[0] + z_plan3d = z - self.coordinates_min[2] + + print(width, depth, n) + + if width < depth: + + + if n>1: + for k in range(n-1): + for i in range(-1, depth+1): + for y in range(-1, width//2+1): + self.editor.placeBlock((x + i, self.coordinates_max[1]+k, z+y+k+3), self.roof) + self.editor.placeBlock((x + i, self.coordinates_max[1]+k, z+depth-y-4-k), self.roof) + if width%2 == 0: + for i in range(-1, depth+1): + self.editor.placeBlock((x+width//2+1, self.coordinates_max[1]+n-1, z+i), self.roof) + for i in range(-1,depth+1): + self.editor.placeBlock((x+width//2, self.coordinates_max[1]+n-1, z+i), self.roof) + + else: + if n>1: + for k in range(n-1): + for i in range(-1, width+1): + for y in range(-1, depth//2+1): + self.editor.placeBlock((x + i, self.coordinates_max[1]+k, z+y+k+2), self.roof) + self.editor.placeBlock((x + i, self.coordinates_max[1]+k, z+depth-y-3-k), self.roof) + if depth%2 == 0: + for i in range(-1, width+1): + self.editor.placeBlock((x+i, self.coordinates_max[1]+n-1, z+depth//2+1), self.roof) + for i in range(-1,width+1): + self.editor.placeBlock((x+i, self.coordinates_max[1]+n-1, z+depth//2), self.roof) + + print('-----------------------------------') + + for i in range(-1, width+1): + for j in range(-1, depth+1): + if width 1: + house.placeStairs() + + +if __name__ == "__main__": + editor = Editor(buffering=True) + buildArea = editor.getBuildArea() + coordinates_min = [min(buildArea.begin[i], buildArea.last[i]) for i in range(3)] + coordinates_max = [max(buildArea.begin[i], buildArea.last[i]) for i in range(3)] + + blocks = { + "wall": "blackstone", + "roof": "blackstone", + "roof_slab": "blackstone_slab", + "door": "oak_door", + "window": "glass_pane", + "entrance": "oak_door", + "stairs": "quartz_stairs", + "stairs_slab": "quartz_slab", + "celling": "quartz_block", + "floor": "quartz_block", + "celling_slab": "quartz_slab", + "garden_outline": "oak_leaves", + "garden_floor": "grass_block" + } + + for i in range(1): + house = House(editor, coordinates_min, coordinates_max,"W", blocks) + + house.build() + + new_coordinates_min =(coordinates_max[0] + 10, coordinates_min[1], coordinates_min[2]) + new_coordinates_max = (coordinates_max[0] + 10 +24, coordinates_max[1], coordinates_max[2]) + coordinates_min = new_coordinates_min + coordinates_max = new_coordinates_max + + # delete(editor, coordinates_min, coordinates_max) + editor.flushBuffer() + + + + \ No newline at end of file diff --git a/main.py b/main.py index ae2568b..285d50d 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,10 @@ +from world_maker.world_maker import * + +def main(): + rectangle_house_mountain, rectangle_building, skeleton_highway, skeleton_mountain = world_maker() + +if __name__ == '__main__': + main() from gdpc import Editor, Block, geometry, Transform import networks.curve as curve import numpy as np diff --git a/world_maker/City.py b/world_maker/City.py index 113e214..e7d0fb4 100644 --- a/world_maker/City.py +++ b/world_maker/City.py @@ -1,7 +1,10 @@ -from District import District -from Position import Position +from world_maker.District import District, Road +from world_maker.Position import Position from PIL import Image -import random +from random import randint +from world_maker.data_analysis import handle_import_image, detect_mountain +from typing import Union +import numpy as np class City: @@ -25,8 +28,8 @@ class City: """ Initialize the maps of the city. It reads the heightmap and watermap images and converts them into 2D lists. """ - heightmap = Image.open('./data/heightmap.png').convert('L') - watermap = Image.open('./data/watermap.png').convert('L') + heightmap = Image.open('./world_maker/data/heightmap.png').convert('L') + watermap = Image.open('./world_maker/data/watermap.png').convert('L') width, height = heightmap.size self.map_data = [[-1 if watermap.getpixel((x, y)) > 0 else 0 for x in range(width)] for y in range(height)] self.height_map = [[heightmap.getpixel((x, y)) for x in range(width)] for y in range(height)] @@ -72,7 +75,6 @@ class City: index_district_chosen = index else: self.districts[index].area_expend.remove(point) - self.districts[index_district_chosen].area.append(point) self.districts[index_district_chosen].area_expend_from_point.append(point) self.districts[index_district_chosen].area_expend.remove(point) self.map_data[point.y][point.x] = index_district_chosen + 1 @@ -81,7 +83,6 @@ class City: """ Update the expansion points of all districts in the city. """ - for district in self.districts: if len(district.area_expend_from_point) > 0: district.update_expend_points(district.area_expend_from_point[0], self.map_data, self.height_map) @@ -104,7 +105,7 @@ class City: """ width, height = len(self.map_data[0]), len(self.map_data) img = Image.new('RGB', (width, height)) - colors = {id_district: (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + colors = {id_district: (randint(0, 255), randint(0, 255), randint(0, 255)) for id_district in range(1, len(self.districts) + 1)} for y in range(height): @@ -114,13 +115,93 @@ class City: else: img.putpixel((x, y), colors[self.map_data[y][x]]) - img.save('./data/district.png') + img.save('./world_maker/data/district.png') print("[City] District map created.") + def draw_roads(self, size_road: int = 1) -> Image: + """ + Draw the roads of the city on the image. + + :param size: + """ + image = Image.new('RGB', Image.open('./world_maker/data/heightmap.png').size) + for district in self.districts: + district.draw_roads(image, size_road) + return image + + def district_generate_road(self) -> list[Road]: + """ + Generate the roads of the city for each district. + + :return: The list of roads of the city. + """ + roads = [] + for district in self.districts: + if district.type != "mountain": + district.generate_roads(self.map_data) + roads.extend(district.roads) + return roads + + def point_in_which_district(self, point: Union[Position, tuple[int, int]]) -> int: + """ + Get the index of the district in which the point is located. + + :param point: The point to check. + :return: The index of the district in which the point is located. + """ + if isinstance(point, Position): + point = (point.x, point.y) + return self.map_data[point[1]][point[0]] + + def get_district_mountain_map(self) -> Image: + """ + Get the map of a district. + + :param district_id: The id of the district. + :return: The map of the district. + """ + district_id = [district.tile_id for district in self.districts if district.type == "mountain"] + array = np.array([[True if self.map_data[y][x] in district_id else False for x in range(len(self.map_data[0]))] + for y in range(len(self.map_data))]) + image = Image.fromarray(array) + image.save('./world_maker/data/mountain_map.png') + return image + + def generate_district(self): + image = handle_import_image('./world_maker/data/smooth_sobel_watermap.png').convert('L') + array = np.array(image) + mountain_coo = detect_mountain() + self.add_district(Position(mountain_coo[0], mountain_coo[1]), "mountain") + print("[City] District added.") + remove_circle_data(array, mountain_coo) + area = get_area_array(array) + sizeX, sizeY = len(array[0]), len(array) + while area > sizeX * sizeY * 0.1: + x, y = randint(0, sizeX - 1), randint(0, sizeY - 1) + if array[y][x]: + self.add_district(Position(x, y)) + remove_circle_data(array, (x, y)) + area = get_area_array(array) + print("[City] District added.") + + +def remove_circle_data(array, center, radius=100): + y_indices, x_indices = np.indices(array.shape) + dist_sq = (y_indices - center[1]) ** 2 + (x_indices - center[0]) ** 2 + mask = dist_sq <= radius ** 2 + array[mask] = False + + +def get_area_array(array) -> int: + return np.sum(array) + if __name__ == '__main__': city = City() for i in range(10): - city.add_district(Position(random.randint(0, 800), random.randint(0, 800))) + city.add_district(Position(randint(0, 400), randint(0, 400))) city.loop_expend_district() city.district_draw_map() + city.district_generate_road() + image = city.draw_roads(Image.new('RGB', (401, 401)), 4) + image.save('./world_maker/data/roadmap.png') diff --git a/world_maker/District.py b/world_maker/District.py index e0e3f9a..6de31ac 100644 --- a/world_maker/District.py +++ b/world_maker/District.py @@ -1,8 +1,24 @@ -from Position import Position +from world_maker.Position import Position +from typing import Union +from random import randint +from PIL import Image + + +class Road: + def __init__(self, position: Position, id_height: int, id_width: int, border: bool = False): + self.position: Position = position + self.north: Union[Road, None] = None + self.south: Union[Road, None] = None + self.east: Union[Road, None] = None + self.west: Union[Road, None] = None + self.id_height = id_height + self.id_width = id_width + self.border = border + class District: """ - The CustomDistrict class represents a district that can be expanded. + The District class represents a district that can be expanded. Attributes: center_expend (Position): The center position from which the district expands. @@ -10,6 +26,7 @@ class District: area_expend_from_point (list): The list of positions from which the district can expand. area_expend (list): The list of positions to which the district will maybe expand. """ + def __init__(self, tile_id: int, center: Position, district_type: str = ""): """ The constructor for the District class. @@ -23,12 +40,14 @@ class District: self.tile_id = tile_id self.type = district_type self.center_expend = center - self.area = [center] self.area_expend_from_point = [center] self.area_expend = [] + self.roads: list[Road] = [] + self.roads_expend = [] - def verify_point(self, point: Position, point_new: Position, map_data: list[list[int]], height_map: list[list[int]]): - """ + def verify_point(self, point: Position, point_new: Position, map_data: list[list[int]], + height_map: list[list[int]]): + """ Verify if a new point can be added to a district extend area list. :param point: The current point. @@ -37,11 +56,34 @@ class District: :param height_map: The 2D list representing the height map. :return: True if the new point can be added, False otherwise. """ - return (0 <= point_new.x < len(map_data[0]) and - 0 <= point_new.y < len(map_data) and - map_data[point_new.y][point_new.x] == 0 and - (self.type == "Mountain" or - abs(height_map[point_new.y][point_new.x] - height_map[point.y][point.x]) < 2)) + return (0 <= point_new.x < len(map_data[0]) and + 0 <= point_new.y < len(map_data) and + map_data[point_new.y][point_new.x] == 0 and + (self.type == "mountain" or + abs(height_map[point_new.y][point_new.x] - height_map[point.y][point.x]) < 2)) + + def is_point_inside(self, point: Position, map_data) -> bool: + """ + Check if a point is inside the district. + + :param point: The point to be checked. + :return: True if the point is inside the district, False otherwise. + """ + if not (0 <= point.x < len(map_data[0]) and 0 <= point.y < len(map_data)): + return False + return map_data[point.y][point.x] == self.tile_id + + def is_position_in_area_expend(self, position: Position) -> bool: + """ + Check if a position is inside the district. + + :param position: The position to be checked. + :return: True if the position is inside the district, False otherwise. + """ + for point in self.area_expend: + if point == position: + return True + return False def update_expend_points(self, point: Position, map_data: list[list[int]], height_map: list[list[int]]): """ @@ -53,6 +95,142 @@ class District: """ for pos in [Position(1, 0), Position(-1, 0), Position(0, 1), Position(0, -1)]: if self.verify_point(point, point + pos, map_data, height_map): - if point + pos not in self.area_expend: + if not self.is_position_in_area_expend(point + pos): self.area_expend.append(point + pos) self.area_expend_from_point.remove(point) + + def move_point_to_area(self, point: Position, vector: Position, map_data) -> Position: + while not self.is_point_inside(point + vector, map_data): + point += vector + return point + vector + + def get_road_from_point(self, point: Position) -> Union[Road, None]: + """ + Get the road that contains a specific point. + + :param point: The point to be checked. + :return: The road that contains the point. + """ + for road in self.roads: + if point == road.position: + return road + return None + + def get_road_expend_from_point(self, point: Position) -> Union[Road, None]: + """ + Get the road that contains a specific point. + + :param point: The point to be checked. + :return: The road that contains the point. + """ + for road in self.roads_expend: + if point == road.position: + return road + return None + + def generate_roads(self, map_data, random_range=(20, 40)): + width = {0: self.center_expend.x} + height = {0: self.center_expend.y} + self.roads_expend = [Road(self.center_expend, 0, 0)] + self.roads = [self.roads_expend[0]] + while len(self.roads_expend) > 0: + road = self.roads_expend.pop(0) + print(road.position) + for id_width in [-1, 1]: + if road.id_width + id_width not in width: + width[road.id_width + id_width] = width[road.id_width] + randint(random_range[0], + random_range[1]) * id_width + road_new = Road(Position(width[road.id_width + id_width], road.position.y), + road.id_height, road.id_width + id_width) + if self.is_point_inside(road_new.position, map_data): + road_search = self.get_road_from_point(road_new.position) + road_expend_search = self.get_road_expend_from_point(road_new.position) + if road_search is not None: + road_new = road_search + + if id_width == -1: + road.west = road_new + road_new.east = road + else: + road.east = road_new + road_new.west = road + + if road_search is None: + self.roads.append(road_new) + self.roads_expend.append(road_new) + else: + self.roads[self.roads.index(road_search)] = road_new + if road_expend_search is not None: + self.roads_expend[self.roads_expend.index(road_expend_search)] = road_new + else: + point_new = self.move_point_to_area(road_new.position, Position(-id_width, 0), map_data) + road_new = Road(point_new, road.id_height, road.id_width + id_width, True) + if id_width == -1: + road.west = road_new + road_new.east = road + else: + road.east = road_new + road_new.west = road + self.roads.append(road_new) + + for id_height in [-1, 1]: + if road.id_height + id_height not in height: + height[road.id_height + id_height] = height[road.id_height] + randint(random_range[0], + random_range[1]) * id_height + road_new = Road(Position(road.position.x, height[road.id_height + id_height]), + road.id_height + id_height, road.id_width) + if self.is_point_inside(road_new.position, map_data): + road_search = self.get_road_from_point(road_new.position) + road_expend_search = self.get_road_expend_from_point(road_new.position) + if road_search is not None: + road_new = road_search + + if id_height == -1: + road.north = road_new + road_new.south = road + else: + road.south = road_new + road_new.north = road + + if road_search is None: + self.roads.append(road_new) + self.roads_expend.append(road_new) + else: + self.roads[self.roads.index(road_search)] = road_new + if road_expend_search is not None: + self.roads_expend[self.roads_expend.index(road_expend_search)] = road_new + else: + pass + point_new = self.move_point_to_area(road_new.position, Position(0, -id_height), map_data) + road_new = Road(point_new, road.id_height + id_height, road.id_width, True) + if id_height == -1: + road.north = road_new + road_new.south = road + else: + road.south = road_new + road_new.north = road + self.roads.append(road_new) + + def draw_roads(self, image: Image, size: int = 1): + for road in self.roads: + image.putpixel((road.position.x, road.position.y), (255, 255, 255)) + if road.north is not None: + for y in range(road.position.y, road.north.position.y): + image = draw_square(image, Position(road.position.x, y), size) + if road.south is not None: + for y in range(road.position.y, road.south.position.y): + image = draw_square(image, Position(road.position.x, y), size) + if road.east is not None: + for x in range(road.position.x, road.east.position.x): + image = draw_square(image, Position(x, road.position.y), size) + if road.west is not None: + for x in range(road.position.x, road.west.position.x): + image = draw_square(image, Position(x, road.position.y), size) + + +def draw_square(image, center: Position, size: int) -> Image: + for x in range(center.x - size, center.x + size): + for y in range(center.y - size, center.y + size): + if 0 <= x < image.width and 0 <= y < image.height: + image.putpixel((x, y), (255, 255, 255)) + return image diff --git a/world_maker/Skeleton.py b/world_maker/Skeleton.py index 6b4d1ce..63a92f9 100644 --- a/world_maker/Skeleton.py +++ b/world_maker/Skeleton.py @@ -3,7 +3,7 @@ import numpy as np from skimage.morphology import skeletonize from skan.csr import skeleton_to_csgraph from collections import Counter -from PIL import Image +from PIL import Image, ImageDraw import random @@ -158,7 +158,7 @@ class Skeleton: # xzDistance = (max(buildRect.end[0], buildRect.begin[0]) - min(buildRect.end[0], buildRect.begin[0]), # max(buildRect.end[1], buildRect.begin[1]) - min(buildRect.end[1], buildRect.begin[1])) - heightmap = Image.open("data/heightmap.png").convert('RGB') + heightmap = Image.open("./world_maker/data/heightmap.png").convert('RGB') # roadsArea = Image.new("L", xzDistance, 0) # width, height = heightmap.size @@ -213,3 +213,30 @@ class Skeleton: # ) print("[Skeleton] Mapping completed.") return heightmap # , roadsArea + + def road_area(self, name: str, radius: int = 10) -> Image: + print("[Skeleton] Start mapping the road area...") + heightmap = Image.open("./world_maker/data/heightmap.png") + width, height = heightmap.size + road_area_map = Image.new("L", (width, height), 0) + road_area_map_draw = ImageDraw.Draw(road_area_map) + + # Lines + for i in range(len(self.lines)): + for j in range(len(self.lines[i])): + z = self.coordinates[self.lines[i][j]][0] + x = self.coordinates[self.lines[i][j]][2] + circle_coords = (z - radius, x - radius, z + radius, x + radius) + road_area_map_draw.ellipse(circle_coords, fill=255) + + # Centers + for i in range(len(self.centers)): + z = self.coordinates[self.centers[i]][0] + x = self.coordinates[self.centers[i]][2] + circle_coords = (z - radius, x - radius, z + radius, x + radius) + road_area_map_draw.ellipse(circle_coords, fill=255) + + road_area_map.save("./world_maker/data/"+name) + + print("[Skeleton] Road area mapping completed.") + return road_area_map diff --git a/world_maker/World.py b/world_maker/World.py index c771332..3fc41c0 100644 --- a/world_maker/World.py +++ b/world_maker/World.py @@ -1,7 +1,7 @@ from gdpc import Editor, geometry, lookup import numpy as np from PIL import Image -from Block import Block +from world_maker.Block import Block waterBiomes = [ "minecraft:ocean", diff --git a/world_maker/data/building.png b/world_maker/data/building.png new file mode 100644 index 0000000..2d26e98 Binary files /dev/null and b/world_maker/data/building.png differ diff --git a/world_maker/data/city_map.png b/world_maker/data/city_map.png new file mode 100644 index 0000000..c7a0dc1 Binary files /dev/null and b/world_maker/data/city_map.png differ diff --git a/world_maker/data/district.png b/world_maker/data/district.png new file mode 100644 index 0000000..0fa7716 Binary files /dev/null and b/world_maker/data/district.png differ diff --git a/world_maker/data/heightmap.png b/world_maker/data/heightmap.png index a743f15..fc75ff3 100644 Binary files a/world_maker/data/heightmap.png and b/world_maker/data/heightmap.png differ diff --git a/world_maker/data/highwaymap.png b/world_maker/data/highwaymap.png index 887f225..7c4d178 100644 Binary files a/world_maker/data/highwaymap.png and b/world_maker/data/highwaymap.png differ diff --git a/world_maker/data/mountain_map.png b/world_maker/data/mountain_map.png new file mode 100644 index 0000000..03ec375 Binary files /dev/null and b/world_maker/data/mountain_map.png differ diff --git a/world_maker/data/negative_sobel_water_map.png b/world_maker/data/negative_sobel_water_map.png deleted file mode 100644 index 893f381..0000000 Binary files a/world_maker/data/negative_sobel_water_map.png and /dev/null differ diff --git a/world_maker/data/roadmap.png b/world_maker/data/roadmap.png new file mode 100644 index 0000000..f277263 Binary files /dev/null and b/world_maker/data/roadmap.png differ diff --git a/world_maker/data/skeleton_highway.png b/world_maker/data/skeleton_highway.png index e6a99cc..e69f2eb 100644 Binary files a/world_maker/data/skeleton_highway.png and b/world_maker/data/skeleton_highway.png differ diff --git a/world_maker/data/skeleton_highway_area.png b/world_maker/data/skeleton_highway_area.png new file mode 100644 index 0000000..4bffcc6 Binary files /dev/null and b/world_maker/data/skeleton_highway_area.png differ diff --git a/world_maker/data/skeleton_mountain.png b/world_maker/data/skeleton_mountain.png new file mode 100644 index 0000000..3a2af61 Binary files /dev/null and b/world_maker/data/skeleton_mountain.png differ diff --git a/world_maker/data/skeleton_mountain_area.png b/world_maker/data/skeleton_mountain_area.png new file mode 100644 index 0000000..3bfc866 Binary files /dev/null and b/world_maker/data/skeleton_mountain_area.png differ diff --git a/world_maker/data/smooth_sobel_watermap.png b/world_maker/data/smooth_sobel_watermap.png new file mode 100644 index 0000000..a2c350d Binary files /dev/null and b/world_maker/data/smooth_sobel_watermap.png differ diff --git a/world_maker/data/sobelmap.png b/world_maker/data/sobelmap.png index b61cb41..e587909 100644 Binary files a/world_maker/data/sobelmap.png and b/world_maker/data/sobelmap.png differ diff --git a/world_maker/data/treemap.png b/world_maker/data/treemap.png index ab753da..83a187a 100644 Binary files a/world_maker/data/treemap.png and b/world_maker/data/treemap.png differ diff --git a/world_maker/data/watermap.png b/world_maker/data/watermap.png index 0f1cdf9..36cfd74 100644 Binary files a/world_maker/data/watermap.png and b/world_maker/data/watermap.png differ diff --git a/world_maker/data_analysis.py b/world_maker/data_analysis.py index fa685a2..5b94e8d 100644 --- a/world_maker/data_analysis.py +++ b/world_maker/data_analysis.py @@ -1,17 +1,19 @@ -import World +from world_maker.World import World from PIL import Image, ImageFilter import numpy as np from scipy import ndimage -from Skeleton import Skeleton +from world_maker.Skeleton import Skeleton from typing import Union +from random import randint +import cv2 def get_data(world: World): print("[Data Analysis] Generating data...") heightmap, watermap, treemap = world.getData() - heightmap.save('./data/heightmap.png') - watermap.save('./data/watermap.png') - treemap.save('./data/treemap.png') + heightmap.save('./world_maker/data/heightmap.png') + watermap.save('./world_maker/data/watermap.png') + treemap.save('./world_maker/data/treemap.png') print("[Data Analysis] Data generated.") return heightmap, watermap, treemap @@ -22,13 +24,14 @@ def handle_import_image(image: Union[str, Image]) -> Image: return image -def filter_negative(image: Image) -> Image: +def filter_negative(image: Union[str, Image]) -> Image: """ Invert the colors of an image. Args: image (image): image to filter """ + image = handle_import_image(image) return Image.fromarray(np.invert(np.array(image))) @@ -146,8 +149,8 @@ def subtract_map(image: Union[str, Image], substractImage: Union[str, Image]) -> def group_map(image1: Union[str, Image], image2: Union[str, Image]) -> Image: - image1 = handle_import_image(image1) - image2 = handle_import_image(image2) + image1 = handle_import_image(image1).convert('L') + image2 = handle_import_image(image2).convert('L') array1 = np.array(image1) array2 = np.array(image2) @@ -179,9 +182,9 @@ def filter_remove_details(image: Union[str, Image], n: int = 20) -> Image: def highway_map() -> Image: print("[Data Analysis] Generating highway map...") - smooth_sobel = filter_smooth("./data/sobelmap.png", 1) + smooth_sobel = filter_smooth("./world_maker/data/sobelmap.png", 1) negative_smooth_sobel = filter_negative(smooth_sobel) - negative_smooth_sobel_water = subtract_map(negative_smooth_sobel, './data/watermap.png') + negative_smooth_sobel_water = subtract_map(negative_smooth_sobel, './world_maker/data/watermap.png') array_sobel_water = np.array(negative_smooth_sobel_water) array_sobel_water = ndimage.binary_erosion(array_sobel_water, iterations=12) array_sobel_water = ndimage.binary_dilation(array_sobel_water, iterations=5) @@ -190,7 +193,7 @@ def highway_map() -> Image: array_sobel_water = filter_smooth_array(array_sobel_water, 6) image = Image.fromarray(array_sobel_water) image_no_details = filter_remove_details(image, 15) - image_no_details.save('./data/highwaymap.png') + image_no_details.save('./world_maker/data/highwaymap.png') print("[Data Analysis] Highway map generated.") return image_no_details @@ -208,16 +211,82 @@ def create_volume(surface: np.ndarray, heightmap: np.ndarray, make_it_flat: bool def convert_2D_to_3D(image: Union[str, Image], make_it_flat: bool = False) -> np.ndarray: image = handle_import_image(image) - heightmap = Image.open('./data/heightmap.png').convert('L') + heightmap = Image.open('./world_maker/data/heightmap.png').convert('L') heightmap = np.array(heightmap) surface = np.array(image) volume = create_volume(surface, heightmap, make_it_flat) return volume -def skeleton_highway_map(image: Union[str, Image] = './data/highwaymap.png'): +def skeleton_highway_map(image: Union[str, Image] = './world_maker/data/highwaymap.png') -> Skeleton: image_array = convert_2D_to_3D(image, True) skeleton = Skeleton(image_array) skeleton.parse_graph(True) heightmap_skeleton = skeleton.map() - heightmap_skeleton.save('./data/skeleton_highway.png') + heightmap_skeleton.save('./world_maker/data/skeleton_highway.png') + skeleton.road_area('skeleton_highway_area.png', 10) + return skeleton + + +def skeleton_mountain_map(image: Union[str, Image] = './world_maker/data/mountain_map.png') -> Skeleton: + image_array = convert_2D_to_3D(image, True) + skeleton = Skeleton(image_array) + skeleton.parse_graph() + heightmap_skeleton = skeleton.map() + heightmap_skeleton.save('./world_maker/data/skeleton_mountain.png') + skeleton.road_area('skeleton_mountain_area.png', 3) + return skeleton + + +def smooth_sobel_water() -> Image: + watermap = handle_import_image("./world_maker/data/watermap.png") + watermap = filter_negative(filter_remove_details(filter_negative(watermap), 5)) + sobel = handle_import_image("./world_maker/data/sobelmap.png") + sobel = filter_remove_details(filter_smooth(sobel, 1), 2) + group = group_map(watermap, sobel) + group = filter_negative(group) + group.save('./world_maker/data/smooth_sobel_watermap.png') + return group + + +def detect_mountain(image: Union[str, Image] = './world_maker/data/sobelmap.png') -> Image: + image = handle_import_image(image) + sobel = np.array(image) + pixels = sobel.reshape((-1, 1)) + pixels = np.float32(pixels) + + criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2) + k = 3 + _, labels, centers = cv2.kmeans(pixels, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS) + + centers = np.uint8(centers) + segmented_image = centers[labels.flatten()] + segmented_image = segmented_image.reshape(sobel.shape) + mountain = segmented_image == segmented_image.max() + + contours, _ = cv2.findContours(mountain.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + max_contour = max(contours, key=cv2.contourArea) + M = cv2.moments(max_contour) + cX = int(M["m10"] / M["m00"]) + cY = int(M["m01"] / M["m00"]) + + print(f"[Data Analysis] The center of the mountain is at ({cX}, {cY})") + return (cX, cY) + + +def rectangle_2D_to_3D(rectangle: list[tuple[tuple[int, int], tuple[int, int]]], + height_min: int = 6, height_max: int = 10) \ + -> list[tuple[tuple[int, int, int], tuple[int, int, int]]]: + image = handle_import_image('./world_maker/data/heightmap.png').convert('L') + new_rectangle = [] + for rect in rectangle: + start, end = rect + avg_height = 0 + for x in range(start[0], end[0]): + for y in range(start[1], end[1]): + avg_height += image.getpixel((x, y)) + avg_height = int(avg_height / ((end[0] - start[0]) * (end[1] - start[1]))) + new_rectangle.append( + ((start[0], avg_height, start[1]), (end[0], avg_height + randint(height_min, height_max), end[1]))) + return new_rectangle diff --git a/world_maker/pack_rectangle.py b/world_maker/pack_rectangle.py new file mode 100644 index 0000000..6f2a8b0 --- /dev/null +++ b/world_maker/pack_rectangle.py @@ -0,0 +1,112 @@ +from PIL import Image +import numpy as np +from typing import Union +from world_maker.data_analysis import handle_import_image +from random import randint + + +class Rectangle: + def __init__(self, width, height): + self.width = width + self.height = height + + +class Bin: + def __init__(self, grid): + self.grid = grid + self.rectangles = [] + + def place_rectangle(self, rectangle): + best_spot = None + best_spot_empty_area = float('inf') + + for i in range(len(self.grid[0]) - rectangle.width + 1): + for j in range(len(self.grid) - rectangle.height + 1): + if self.can_place(rectangle, i, j): + empty_area = self.calculate_empty_area(rectangle, i, j) + if empty_area < best_spot_empty_area: + best_spot = (i, j) + best_spot_empty_area = empty_area + + if best_spot is not None: + self.rectangles.append((best_spot, (best_spot[0] + rectangle.width, best_spot[1] + rectangle.height))) + self.update_grid(rectangle, *best_spot) + return True + + return False + + def calculate_empty_area(self, rectangle, x, y): + empty_area = 0 + for rect_x in range(x, x + rectangle.width): + for rect_y in range(y, y + rectangle.height): + if self.grid[rect_y][rect_x]: + empty_area += 1 + return empty_area + + def can_place(self, rectangle, x, y): + for rect_x in range(x, x + rectangle.width): + for rect_y in range(y, y + rectangle.height): + if not self.grid[rect_y][rect_x]: + return False + return True + + def update_grid(self, rectangle, x, y): + for rect_x in range(x, x + rectangle.width): + for rect_y in range(y, y + rectangle.height): + self.grid[rect_y][rect_x] = False + + +def pack_rectangles(rectangles, grid): + rectangles = sorted(rectangles, key=lambda r: r.width * r.height, reverse=True) + bins = [Bin(grid)] + + for rectangle in rectangles: + for bin in bins: + if bin.place_rectangle(rectangle): + break + else: + new_bin = Bin(grid) + if new_bin.place_rectangle(rectangle): + bins.append(new_bin) + else: + return False + + return True + + +def generate_rectangle(min_width: int = 10, max_width: int = 25): + width = randint(min_width, max_width) + height = randint(min_width, max_width) + return Rectangle(width, height) + + +def pack_rectangles(grid, min_width: int = 10, max_width: int = 25): + bin = Bin(grid) + while True: + rectangle = generate_rectangle(min_width, max_width) + if not bin.place_rectangle(rectangle): + break + print(len(bin.rectangles)) + return bin.rectangles + + +def draw_rectangles(rectangles, grid): + image = Image.new('RGB', (len(grid[0]), len(grid)), (0, 0, 0)) + for rectangle in rectangles: + start, end = rectangle + for x in range(start[0], end[0]): + for y in range(start[1], end[1]): + image.putpixel((x, y), (144, 255, 144)) + return image + + +def generate_building(image: Union[str, Image], min_width: int = 10, max_width: int = 25): + image = handle_import_image(image).convert('L') + grid = np.array(image) + rectangles = pack_rectangles(grid, min_width, max_width) + draw_rectangles(rectangles, grid).save('./world_maker/data/building.png') + return rectangles + + +if __name__ == '__main__': + generate_building() diff --git a/world_maker/world_maker.py b/world_maker/world_maker.py index c711b88..8455aba 100644 --- a/world_maker/world_maker.py +++ b/world_maker/world_maker.py @@ -1,9 +1,34 @@ -import World +from world_maker.World import World from PIL import Image -from data_analysis import get_data, highway_map, filter_sobel, skeleton_highway_map +from world_maker.data_analysis import (get_data,filter_negative, rectangle_2D_to_3D, skeleton_mountain_map, highway_map, filter_sobel, skeleton_highway_map, \ + smooth_sobel_water, subtract_map, detect_mountain) +from world_maker.City import City +from world_maker.Position import Position +from random import randint +from world_maker.pack_rectangle import generate_building -if __name__ == '__main__': - world = World.World() +def world_maker(): + world = World() heightmap, watermap, treemap = get_data(world) - filter_sobel("./data/heightmap.png").save('./data/sobelmap.png') - skeleton_highway_map(highway_map()) + filter_sobel("./world_maker/data/heightmap.png").save('./world_maker/data/sobelmap.png') + smooth_sobel_water_map = smooth_sobel_water() + skeleton_highway = skeleton_highway_map(highway_map()) + city = City() + city.generate_district() + city.loop_expend_district() + city.district_draw_map() + city.district_generate_road() + image_mountain_map = city.get_district_mountain_map() + road = city.draw_roads(4) + road.save('./world_maker/data/roadmap.png') + subtract_map(smooth_sobel_water_map, road).save('./world_maker/data/city_map.png') + subtract_map('./world_maker/data/city_map.png', './world_maker/data/skeleton_highway_area.png').save('./world_maker/data/city_map.png') + subtract_map('./world_maker/data/city_map.png', './world_maker/data/mountain_map.png').save('./world_maker/data/city_map.png') + rectangle_building = generate_building('./world_maker/data/city_map.png') + rectangle_building = rectangle_2D_to_3D(rectangle_building) + + skeleton_mountain = skeleton_mountain_map(image_mountain_map) + subtract_map('./world_maker/data/mountain_map.png', './world_maker/data/skeleton_mountain_area.png').save('./world_maker/data/mountain_map.png') + subtract_map(smooth_sobel_water_map, filter_negative('./world_maker/data/mountain_map.png')).save('./world_maker/data/mountain_map.png') + rectangle_mountain = generate_building('./world_maker/data/mountain_map.png') + return rectangle_building, rectangle_mountain, skeleton_highway, skeleton_mountain