diff --git a/README.md b/README.md index 337d968..9b006e7 100644 --- a/README.md +++ b/README.md @@ -397,6 +397,7 @@ Let's have a closer look at the fields: - `background` - set it to True for the textures, which should have impact on the Background Terrain, by default it's used to subtract the water depth from the DEM and background terrain. - `info_layer` - if the layer is saving some data in JSON format, this section will describe it's name in the JSON file. Used to find the needed JSON data, for example for fields it will be `fields` and as a value - list of polygon coordinates. - `invisible` - set it to True for the textures, which should not be drawn in the files, but only to save the data in the JSON file (related to the previous field). +- `procedural` - is a list of corresponding files, that will be used for a procedural generation. For example: `"procedural": ["PG_meadow", "PG_acres"]` - means that the texture will be used for two procedural generation files: `masks/PG_meadow.png` and `masks/PG_acres.png`. Note, that the one procuderal name can be applied to multiple textures, in this case they will be merged into one mask. ## Background terrain The tool now supports the generation of the background terrain. If you don't know what it is, here's a brief explanation. The background terrain is the world around the map. It's important to create it because if you don't, the map will look like it's floating in the void. The background terrain is a simple plane that can (and should) be textured to look fine.
diff --git a/data/fs25-map-template.zip b/data/fs25-map-template.zip index 98b509f..677c21a 100644 Binary files a/data/fs25-map-template.zip and b/data/fs25-map-template.zip differ diff --git a/data/fs25-texture-schema.json b/data/fs25-texture-schema.json index 677d0f5..b9bee1c 100644 --- a/data/fs25-texture-schema.json +++ b/data/fs25-texture-schema.json @@ -19,7 +19,8 @@ "width": 8, "color": [70, 70, 70], "priority": 1, - "info_layer": "roads" + "info_layer": "roads", + "procedural": ["PG_roads"] }, { "name": "asphaltGravel", @@ -34,7 +35,8 @@ "count": 2, "tags": { "building": true }, "width": 8, - "color": [130, 130, 130] + "color": [130, 130, 130], + "procedural": ["PG_buildings"] }, { "name": "concreteGravelSand", @@ -71,7 +73,8 @@ "tags": { "natural": "grassland" }, "color": [34, 255, 34], "priority": 0, - "usage": "grass" + "usage": "grass", + "procedural": ["PG_grass"] }, { "name": "grassClovers", @@ -88,7 +91,8 @@ "width": 2, "color": [11, 66, 0], "usage": "forest", - "priority": 5 + "priority": 5, + "procedural": ["PG_forest"] }, { "name": "grassDirtPatchyDry", @@ -140,7 +144,8 @@ "tags": { "highway": ["secondary", "tertiary", "road", "service"] }, "width": 4, "color": [140, 180, 210], - "info_layer": "roads" + "info_layer": "roads", + "procedural": ["PG_roads"] }, { "name": "mudDark", @@ -148,7 +153,8 @@ "tags": { "landuse": ["farmland", "meadow"] }, "color": [47, 107, 85], "priority": 4, - "info_layer": "fields" + "info_layer": "fields", + "procedural": ["PG_meadow", "PG_acres"] }, { "name": "mudDarkGrassPatchy", @@ -221,6 +227,7 @@ }, "width": 10, "color": [255, 20, 20], - "background": true + "background": true, + "procedural": ["PG_water"] } ] diff --git a/maps4fs/generator/texture.py b/maps4fs/generator/texture.py index 3f74ee8..0783fb1 100644 --- a/maps4fs/generator/texture.py +++ b/maps4fs/generator/texture.py @@ -5,6 +5,7 @@ import json import os import re +import shutil from collections import defaultdict from typing import Any, Callable, Generator, Optional @@ -69,6 +70,7 @@ def __init__( # pylint: disable=R0917 usage: str | None = None, background: bool = False, invisible: bool = False, + procedural: list[str] | None = None, ): self.name = name self.count = count @@ -81,6 +83,7 @@ def __init__( # pylint: disable=R0917 self.usage = usage self.background = background self.invisible = invisible + self.procedural = procedural def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore """Returns dictionary with layer data. @@ -99,6 +102,7 @@ def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore "usage": self.usage, "background": self.background, "invisible": self.invisible, + "procedural": self.procedural, } data = {k: v for k, v in data.items() if v is not None} @@ -212,6 +216,10 @@ def preprocess(self) -> None: self._weights_dir = self.game.weights_dir_path(self.map_directory) self.logger.debug("Weights directory: %s.", self._weights_dir) + self.procedural_dir = os.path.join(self._weights_dir, "masks") + os.makedirs(self.procedural_dir, exist_ok=True) + self.logger.debug("Procedural directory: %s.", self.procedural_dir) + self.info_save_path = os.path.join(self.map_directory, "generation_info.json") self.logger.debug("Generation info save path: %s.", self.info_save_path) @@ -251,11 +259,56 @@ def get_layer_by_usage(self, usage: str) -> Layer | None: return layer return None - def process(self): + def process(self) -> None: + """Processes the data to generate textures.""" self._prepare_weights() self._read_parameters() self.draw() self.rotate_textures() + self.copy_procedural() + + def copy_procedural(self) -> None: + """Copies some of the textures to use them as mask for procedural generation. + Creates an empty blockmask if it does not exist.""" + blockmask_path = os.path.join(self.procedural_dir, "BLOCKMASK.png") + if not os.path.isfile(blockmask_path): + self.logger.debug("BLOCKMASK.png not found, creating an empty file.") + img = np.zeros((self.map_size, self.map_size), dtype=np.uint8) + cv2.imwrite(blockmask_path, img) # pylint: disable=no-member + + pg_layers_by_type = defaultdict(list) + for layer in self.layers: + if layer.procedural: + # Get path to the original file. + texture_path = layer.get_preview_or_path(self._weights_dir) + for procedural_layer_name in layer.procedural: + pg_layers_by_type[procedural_layer_name].append(texture_path) + + if not pg_layers_by_type: + self.logger.debug("No procedural layers found.") + return + + for procedural_layer_name, texture_paths in pg_layers_by_type.items(): + procedural_save_path = os.path.join(self.procedural_dir, f"{procedural_layer_name}.png") + if len(texture_paths) > 1: + # If there are more than one texture, merge them. + merged_texture = np.zeros((self.map_size, self.map_size), dtype=np.uint8) + for texture_path in texture_paths: + # pylint: disable=E1101 + texture = cv2.imread(texture_path, cv2.IMREAD_UNCHANGED) + merged_texture[texture == 255] = 255 + cv2.imwrite(procedural_save_path, merged_texture) # pylint: disable=no-member + self.logger.debug( + "Procedural file %s merged from %s textures.", + procedural_save_path, + len(texture_paths), + ) + elif len(texture_paths) == 1: + # Otherwise, copy the texture. + shutil.copyfile(texture_paths[0], procedural_save_path) + self.logger.debug( + "Procedural file %s copied from %s.", procedural_save_path, texture_paths[0] + ) def rotate_textures(self) -> None: """Rotates textures of the layers which have tags."""