diff --git a/world_maker/Block.py b/world_maker/Block.py new file mode 100644 index 0000000..33733ee --- /dev/null +++ b/world_maker/Block.py @@ -0,0 +1,34 @@ +from gdpc import Editor, Block, geometry +from gdpc.lookup import * +import numpy as np +import math + +SOLID_NATURAL_BLOCKS = SOILS | STONES | ORES | LIQUIDS + +class Block: + def __init__(self, coordinates:tuple, name:str): + self.coordinates = coordinates + self.name = name + self.neighbors = [] + self.surface = None + + + def addNeighbors(self, neighbors:list[Block]): + for neighbor in neighbors: + self.neighbors.append(neighbor) + + def isSurface(self): + if self.surface == None: + if str(self.name) in SOLID_NATURAL_BLOCKS: + for neighbor in self.neighbors: + if str(neighbor.name) not in SOLID_NATURAL_BLOCKS: + self.surface = True + return True + if len(self.neighbors) != 0: + self.surface = False + return False + else: + self.surface = False + return False + else: + return self.surface \ No newline at end of file diff --git a/world_maker/City.py b/world_maker/City.py new file mode 100644 index 0000000..1e8574f --- /dev/null +++ b/world_maker/City.py @@ -0,0 +1,126 @@ +from District import District, CustomDistrict, VoronoiDistrict +from Position import Position +from PIL import Image +import random + + +class City: + """ + Attributes: + districts (list): The list of districts in the city. + map_data (list): The 2D list representing the map of the city. + height_map (list): The 2D list representing the height map of the city. + """ + + def __init__(self): + """ + The constructor for the City class. + """ + self.districts = [] + self.map_data = [] + self.height_map = [] + self.init_maps() + + def init_maps(self): + """ + 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') + 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)] + watermap.close() + heightmap.close() + + def add_district(self, center: Position): + """ + Add a new district to the city. + + :param center: The center position of the new district. + """ + self.districts.append(CustomDistrict(len(self.districts) + 1, center)) + self.map_data[center.y][center.x] = len(self.districts) + + def is_expend_finished(self): + """ + Check if the expansion of all districts in the city is finished. + + :return: True if the expansion is finished, False otherwise. + """ + for district in self.districts: + if len(district.area_expend_from_point) > 0: + return False + return True + + def choose_expend_point(self, point: Position, index_district: int): + """ + Choose a point to expand a district based on the distance between the center of the district and the point itself. + + :param point: The point to be expanded. + :param index_district: The index of the district to be expanded. + """ + min_distance = point.distance_to(self.districts[index_district].center_expend) + index_district_chosen = index_district + for i in range(index_district + 1, len(self.districts)): + if point in self.districts[i].area_expend: + distance = point.distance_to(self.districts[i].center_expend) + if distance < min_distance: + min_distance = distance + self.districts[index_district_chosen].area_expend.remove(point) + index_district_chosen = i + else: + self.districts[i].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 + + def update_expend_district(self): + """ + 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) + for district in self.districts: + for point in district.area_expend: + self.choose_expend_point(point, district.tile_id - 1) + + def loop_expend_district(self): + """ + Loop the expansion of all districts in the city until all districts are fully expanded. + """ + loop_count = 0 + while not self.is_expend_finished(): + self.update_expend_district() + loop_count += 1 + if loop_count % 100 == 0: + print("[City] Loop count: ", loop_count) + + def custom_district_draw_map(self): + """ + Draw the map of the city with different colors for each district. + """ + width, height = len(self.map_data[0]), len(self.map_data) + img = Image.new('RGB', (width, height)) + colors = {i: (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + for i in range(1, len(self.districts) + 1)} + + for y in range(height): + for x in range(width): + if self.map_data[y][x] <= 0: + img.putpixel((x, y), (0, 0, 0)) + else: + img.putpixel((x, y), colors[self.map_data[y][x]]) + + img.save('./data/custom_district.png') + + +if __name__ == '__main__': + city = City() + for i in range(10): + city.add_district(Position(random.randint(0, 600), random.randint(0, 600))) + city.loop_expend_district() + city.custom_district_draw_map() diff --git a/world_maker/District.py b/world_maker/District.py new file mode 100644 index 0000000..da218d1 --- /dev/null +++ b/world_maker/District.py @@ -0,0 +1,90 @@ +from Position import Position + + +class District: + """ + The District class represents a district in the world. + A district can be characterized by its type and its unique id. + + Attributes: + tile_id (int): The unique id of the district. + type (str): The type of the district. Can be "Forest", "City", "Mountain" or "Villa". + """ + + def __init__(self, tile_id: int): + """ + The constructor for the District class. + + :param tile_id: Unique id of the district (Must be greater than 0) + """ + if tile_id <= 0: + raise ValueError("Tile id must be greater than 0") + self.tile_id = tile_id + self.type = "" #Forest, City, Montain, Villa + + +def verify_point(point: Position, point_new: Position, map_data: list[list[int]], height_map: list[list[int]]): + """ + Function to verify if a new point can be added to a district extend area list. + + :param point: The current point. + :param point_new: The new point to be verified. + :param map_data: The 2D list representing the map. + :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 + abs(height_map[point_new.y][point_new.x] - height_map[point.y][point.x]) < 2) + + +class CustomDistrict(District): + """ + The CustomDistrict class represents a district that can be expanded. + + Attributes: + center_expend (Position): The center position from which the district expands. + area (list): The list of positions that are part of the 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): + """ + The constructor for the CustomDistrict class. + + :param tile_id: Unique id of the district (Must be greater than 0) + :param center: The center position from which the district expands. + """ + super().__init__(tile_id) + self.center_expend = center + self.area = [center] + self.area_expend_from_point = [center] + self.area_expend = [] + + def update_expend_points(self, point: Position, map_data: list[list[int]], height_map: list[list[int]]): + """ + Update the points to which the district can expand. + + :param point: The current point. + :param map_data: The 2D list representing the map. + :param height_map: The 2D list representing the height map. + """ + for pos in [Position(1, 0), Position(-1, 0), Position(0, 1), Position(0, -1)]: + if verify_point(point, point + pos, map_data, height_map): + if point + pos not in self.area_expend: + self.area_expend.append(point + pos) + self.area_expend_from_point.remove(point) + + +class Edge: #I'm Edging rn + def __init__(self, point1, point2): + self.point1 = point1 + self.point2 = point2 + + +class VoronoiDistrict(District): + def __init__(self, tile_id: int, center: Position): + super().__init__(tile_id) + self.center = center + self.edges = [] diff --git a/world_maker/Position.py b/world_maker/Position.py new file mode 100644 index 0000000..a488037 --- /dev/null +++ b/world_maker/Position.py @@ -0,0 +1,37 @@ +from math import sqrt, atan2 + + +class Position: + def __init__(self, x: int = 0, y: int = 0): + self.x = x + self.y = y + + def __add__(self, other: "Position") -> "Position": + return Position(self.x + other.x, self.y + other.y) + + def __sub__(self, other: "Position") -> "Position": + return Position(self.x - other.x, self.y - other.y) + + def __mul__(self, other: float) -> "Position": + return Position(int(self.x * other), int(self.y * other)) + + def __truediv__(self, other: float) -> "Position": + return Position(int(self.x / other), int(self.y / other)) + + def __str__(self): + return f"({self.x}, {self.y})" + + def __eq__(self, other: "Position"): + return self.x == other.x and self.y == other.y + + def get_tuple(self) -> tuple[int, int]: + return self.x, self.y + + def distance_to(self, other: "Position") -> float: + return sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2) + + def norm(self) -> float: + return sqrt(self.x ** 2 + self.y ** 2) + + def angle_to(self, other: "Position") -> float: + return atan2(self.y - other.y, other.x - self.x) diff --git a/world_maker/World.py b/world_maker/World.py new file mode 100644 index 0000000..f914df4 --- /dev/null +++ b/world_maker/World.py @@ -0,0 +1,237 @@ +from gdpc import Editor, geometry, lookup +import numpy as np +from PIL import Image +from Block import Block + +waterBiomes = [ + "minecraft:ocean", + "minecraft:deep_ocean", + "minecraft:warm_ocean", + "minecraft:lukewarm_ocean", + "minecraft:deep_lukewarm_ocean", + "minecraft:cold_ocean", + "minecraft:deep_cold_ocean", + "minecraft:frozen_ocean", + "minecraft:deep_frozen_ocean", + "minecraft:mushroom_fieds", + "minecraft:river", + "minecraft:frozen_river", +] + +waterBlocks = [ + "minecraft:water", +] + + +class World: + def __init__(self): + + editor = Editor(buffering=True) + buildArea = editor.getBuildArea() + + self.coordinates_min = [min(buildArea.begin[i], buildArea.last[i]) for i in range(3)] + self.coordinates_max = [max(buildArea.begin[i], buildArea.last[i]) for i in range(3)] + + self.length_x = self.coordinates_max[0] - self.coordinates_min[0] + 1 + self.length_y = self.coordinates_max[1] - self.coordinates_min[1] + 1 + self.length_z = self.coordinates_max[2] - self.coordinates_min[2] + 1 + + self.volume = [[[None for _ in range(self.length_z)] for _ in range(self.length_y)] for _ in + range(self.length_x)] + + def isInVolume(self, coordinates): + if (self.coordinates_min[0] <= coordinates[0] <= self.coordinates_max[0] and + self.coordinates_min[1] <= coordinates[1] <= self.coordinates_max[1] and + self.coordinates_min[2] <= coordinates[2] <= self.coordinates_max[2]): + return True + return False + + def addBlocks(self, blocks: list[Block]): + """ + Add block or list of block to the volume. + """ + + for block in blocks: + if self.isInVolume(block.coordinates): + self.volume[block.coordinates[0] - self.coordinates_min[0]][ + block.coordinates[1] - self.coordinates_min[1]][ + block.coordinates[2] - self.coordinates_min[2]] = block + + def removeBlock(self, volumeCoordinates): + """ + Add block or list of block to the volume. + """ + + self.volume[volumeCoordinates[0]][volumeCoordinates[1]][volumeCoordinates[2]] = None + + def getBlockFromCoordinates(self, coordinates): + """ + Use already created volume to get block data. + """ + + editor = Editor(buffering=True) + if self.volume[coordinates[0] - self.coordinates_min[0]][coordinates[1] - self.coordinates_min[1]][ + coordinates[2] - self.coordinates_min[2]] == None: + self.volume[coordinates[0] - self.coordinates_min[0]][coordinates[1] - self.coordinates_min[1]][ + coordinates[2] - self.coordinates_min[2]] = Block((coordinates[0], coordinates[1], coordinates[2]), + editor.getBlock((coordinates[0], coordinates[1], + coordinates[2])).id) + + return self.volume[coordinates[0] - self.coordinates_min[0]][coordinates[1] - self.coordinates_min[1]][ + coordinates[2] - self.coordinates_min[2]] + + def getNeighbors(self, Block): + for i in range(-1, 2): + for j in range(-1, 2): + for k in range(-1, 2): + if not (i == 0 and j == 0 and k == 0): + coordinates = (Block.coordinates[0] + i, Block.coordinates[1] + j, Block.coordinates[2] + k) + if self.isInVolume(coordinates): + Block.addNeighbors([self.getBlockFromCoordinates(coordinates)]) + + def setVolume(self): + """ + Scan the world with no optimization. Not tested on large areas. + """ + + editor = Editor(buffering=True) + + for x in range(self.coordinates_min[0], self.coordinates_max[0] + 1): + for y in range(self.coordinates_min[1], self.coordinates_max[1] + 1): + for z in range(self.coordinates_min[2], self.coordinates_max[2] + 1): + self.addBlocks([Block((x, y, z), editor.getBlock((x, y, z)).id)]) + + def getData(self): + """ + Generate all needed datas for the generator : heightmap, watermap, and preset the volume with data from the heightmap. + """ + + editor = Editor() + buildArea = editor.getBuildArea() + buildRect = buildArea.toRect() + + xzStart = buildRect.begin + print(xzStart, "xzStart") + 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])) + watermap = Image.new("L", xzDistance, 0) + heightmap = Image.new("RGBA", xzDistance, 0) + treesmap = Image.new("RGBA", xzDistance, 0) + + slice = editor.loadWorldSlice(buildRect) + + heightmapData = list(np.array(slice.heightmaps["MOTION_BLOCKING_NO_LEAVES"], dtype=np.uint8)) + treesmapData = list(np.array(slice.heightmaps["MOTION_BLOCKING"], dtype=np.uint8)) + + for x in range(0, xzDistance[0]): + for z in range(0, xzDistance[1]): + + y = heightmapData[x][z] - 1 + yTree = treesmapData[x][z] - 1 + + print('getData', xzStart[0] + x, y, xzStart[1] + z) + + biome = slice.getBiome((x, y, z)) + block = slice.getBlock((x, y, z)) + maybeATree = slice.getBlock((x, yTree, z)) + + if maybeATree.id in lookup.TREES: + treesmap.putpixel((x, z), (yTree, yTree, yTree)) + + if block.id not in lookup.TREES: + heightmap.putpixel((x, z), (y, y, y)) + else: + height = 0 + number = 0 + for i in range(-1, 2): + for j in range(-1, 2): + if (i != 0 or j != 0): + if (0 <= x + i < xzDistance[0]) and (0 <= z + j < xzDistance[1]): + k = heightmapData[x + i][z + j] - 1 + + print('getData for tree', xzStart[0] + x + i, k, xzStart[1] + z + j) + + blockNeighbor = slice.getBlock((x + i, k, z + j)) + if blockNeighbor.id not in lookup.TREES: + height += k + number += 1 + if number != 0: + average = round(height / number) + print(average, "average") + heightmap.putpixel((x, z), (average, average, average)) + + if ((biome in waterBiomes) or (block.id in waterBlocks)): + watermap.putpixel((x, z), (255)) + else: + watermap.putpixel((x, z), (0)) + + self.addBlocks([Block((xzStart[0] + x, 100, xzStart[1] + z), block)]) # y set to 100 for 2D + + return heightmap, watermap, treesmap + + def propagate(self, coordinates, scanned=[]): + print('propagate', coordinates) + i = 0 + editor = Editor(buffering=True) + if self.isInVolume(coordinates): + Block = self.getBlockFromCoordinates(coordinates) + self.getNeighbors(Block) + for neighbor in Block.neighbors: + if neighbor not in scanned: + scanned.append(neighbor) + self.getNeighbors(neighbor) + if neighbor.isSurface(): + self.propagate(neighbor.coordinates, scanned) + + def volumeTo3DBinaryImage(self): + binaryImage = [] + for x in range(self.length_x): + binaryImage.append([]) + for y in range(self.length_y): + binaryImage[x].append([]) + for z in range(self.length_z): + if (self.volume[x][y][z] != None): + binaryImage[x][y].append(True) + else: + binaryImage[x][y].append(False) + + return np.array(binaryImage) + + def maskVolume(self, mask): + """ + + Delete unusable area of the volume to not let it be use by the skeletonize, based on a filtered image that act as a mask. + + Args: + mask (image): white or black image : combined watermap smoothed and sobel smoothed. + """ + editor = Editor() + buildArea = editor.getBuildArea() + buildRect = buildArea.toRect() + + xzStart = buildRect.begin + 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])) + + mask = Image.open(mask) + + slice = editor.loadWorldSlice(buildRect) + + heightmapData = list(np.array(slice.heightmaps["MOTION_BLOCKING_NO_LEAVES"], dtype=np.uint8)) + + for x in range(0, xzDistance[0]): + for z in range(0, xzDistance[1]): + y = heightmapData[x][z] - 1 + if mask.getpixel((x, z)) == (255): + self.removeBlock((x, 100, z)) # y set to 100 for 2D + + def simplifyVolume(self): + array = self.volumeTo3DBinaryImage() + # array = ndimage.binary_dilation(array, iterations=15) + + return array + + +if __name__ == "__main__": + w = World() + w.getData() \ No newline at end of file diff --git a/world_maker/data/custom_district.png b/world_maker/data/custom_district.png new file mode 100644 index 0000000..b9b8b3c Binary files /dev/null and b/world_maker/data/custom_district.png differ diff --git a/world_maker/data/heightmap.png b/world_maker/data/heightmap.png new file mode 100644 index 0000000..e408aea Binary files /dev/null and b/world_maker/data/heightmap.png differ diff --git a/world_maker/data/treemap.png b/world_maker/data/treemap.png new file mode 100644 index 0000000..a8afb9f Binary files /dev/null and b/world_maker/data/treemap.png differ diff --git a/world_maker/data/watermap.png b/world_maker/data/watermap.png new file mode 100644 index 0000000..05c37e8 Binary files /dev/null and b/world_maker/data/watermap.png differ diff --git a/world_maker/world_maker.py b/world_maker/world_maker.py new file mode 100644 index 0000000..1ff126f --- /dev/null +++ b/world_maker/world_maker.py @@ -0,0 +1,17 @@ +import World + + +def get_data(world: World): + heightmap, watermap, treemap = world.getData() + heightmap.save('./data/heightmap.png') + watermap.save('./data/watermap.png') + treemap.save('./data/treemap.png') + + +def main(): + world = World.World() + get_data(world) + + +if __name__ == '__main__': + main()