diff --git a/README.md b/README.md index 89c6aee..94b68a5 100644 --- a/README.md +++ b/README.md @@ -427,20 +427,26 @@ The tool supports the custom size of the map. To use this feature select `Custom ⛔️ Do not use this feature, if you don't know what you're doing. In most cases, the Giants Editor will just crash on opening the file, because you need to enter specific values for the map size.

-![Advanced settings](https://github.com/user-attachments/assets/e7406adf-6b82-41a0-838a-13dd8877bebf) +![Advanced settings](https://github.com/user-attachments/assets/9e8e178a-58d9-4aa6-aefd-4ed53408701d) You can also apply some advanced settings to the map generation process. Note that they're ADVANCED, so you don't need to use them if you're not sure what they do.
-Here's the list of the advanced settings: +### DEM Advanced settings -- DEM multiplier: the height of the map is multiplied by this value. So the DEM map is just a 16-bit grayscale image, which means that the maximum available value there is 65535, while the actual difference between the deepest and the highest point on Earth is about 20 km. Just note that this setting mostly does not matter, because you can always adjust it in the Giants Editor, learn more about the DEM file and the heightScale parameter in [docs](docs/dem.md). By default, it's set to 1. +- Multiplier: the height of the map is multiplied by this value. So the DEM map is just a 16-bit grayscale image, which means that the maximum available value there is 65535, while the actual difference between the deepest and the highest point on Earth is about 20 km. Just note that this setting mostly does not matter, because you can always adjust it in the Giants Editor, learn more about the DEM file and the heightScale parameter in [docs](docs/dem.md). By default, it's set to 1. -- DEM Blur radius: the radius of the Gaussian blur filter applied to the DEM map. By default, it's set to 21. This filter just makes the DEM map smoother, so the height transitions will be more natural. You can set it to 1 to disable the filter, but it will result in a Minecraft-like map. +- Blur radius: the radius of the Gaussian blur filter applied to the DEM map. By default, it's set to 21. This filter just makes the DEM map smoother, so the height transitions will be more natural. You can set it to 1 to disable the filter, but it will result in a Minecraft-like map. -- DEM Plateau height: this value will be added to each pixel of the DEM image, making it "higher". It's useful when you want to add some negative heights on the map, that appear to be in a "low" place. By default, it's set to 0. +- Plateau height: this value will be added to each pixel of the DEM image, making it "higher". It's useful when you want to add some negative heights on the map, that appear to be in a "low" place. By default, it's set to 0. + +### Background Terrain Advanced settings - Background Terrain Generate only full tiles: if checked (by default) the small tiles (N, NE, E, and so on) will not be generated, only the full tile will be created. It's useful when you don't want to work with separate tiles, but with one big file. Since the new method of cutting the map from the background terrain added to the documentation, and now it's possible to perfectly align the map with the background terrain, this option will remain just as a legacy one. +### Texture Advanced settings + +- Fields padding - this value (in meters) will be applied to each field, making it smaller. It's useful when the fields are too close to each other and you want to make them smaller. By default, it's set to 0. + ## Resources In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.
To create a basic map, you only need the Giants Editor. But if you want to create a background terrain - the world around the map, so it won't look like it's floating in the void - you also need Blender and the Blender Exporter Plugins. To create realistic textures for the background terrain, the QGIS is required to obtain high-resolution satellite images.
diff --git a/docs/FAQ.md b/docs/FAQ.md index 8ff7f1a..77ee600 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -29,9 +29,21 @@ You can find the detailed tutorial [here](https://github.com/iwatkot/maps4fs/blo You can find the detailed tutorial [here](https://github.com/iwatkot/maps4fs/blob/main/docs/import_to_giants_editor.md). -### I have some graphic glitches in Giants Editor: screen keeps blinking black, and the terrain become purple at some angles, what should I do? +### I have some graphic glitches in Giants Editor: the terrain become purple at some angles, what should I do? To fix this issue, select the **terrain** object, open the **Terrain** tab in the **Attributes** window, scroll down to the end and press the **Reload material** button. It should help. +### I have some graphic glitches in Giants Editor: the screen keeps flickering black, what should I do? + +To fix this issue, in the Giants Editor click on **Scripts** -> **Create new script**, give it a name and paste the code below: + +``` +setAudioCullingWorldProperties(-8192, -100, -8192, 8192, 500, 8192, 16, 0, 9000) +setLightCullingWorldProperties(-8192, -100, -8192, 8192, 500, 8192, 16, 0, 9000) +setShapeCullingWorldProperties(-8192, -100, -8192, 8192, 500, 8192, 16, 0, 9000) +``` + +Make sure that **Always loaded** checkbox is checked, then save it and execute. It should help. + If you think that some question should be added here, please, contact me in [Discord](https://discord.gg/Sj5QKKyE42) or open an issue on GitHub. Thank you! \ No newline at end of file diff --git a/maps4fs/generator/background.py b/maps4fs/generator/background.py index 720ce69..de79ad6 100644 --- a/maps4fs/generator/background.py +++ b/maps4fs/generator/background.py @@ -184,6 +184,7 @@ def generate_obj_files(self) -> None: dem_data = cv2.imread(tile.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member self.plane_from_np(tile.code, dem_data, save_path) # type: ignore + # pylint: disable=too-many-locals def cutout(self, dem_path: str) -> str: """Cuts out the center of the DEM (the actual map) and saves it as a separate file. @@ -205,20 +206,40 @@ def cutout(self, dem_path: str) -> str: output_size = self.map_height + 1 - # pylint: disable=no-member - dem_data = cv2.resize(dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR) - main_dem_path = self.game.dem_file_path(self.map_directory) + dem_directory = os.path.dirname(main_dem_path) try: os.remove(main_dem_path) except FileNotFoundError: pass - cv2.imwrite(main_dem_path, dem_data) # pylint: disable=no-member - self.logger.info("DEM cutout saved: %s", main_dem_path) + # pylint: disable=no-member + resized_dem_data = cv2.resize( + dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR + ) + + # Giant Editor contains a bug for large maps, where the DEM should not match + # the UnitsPerPixel value. For example, for map 8192x8192, without bug + # the DEM image should be 8193x8193, but it does not work, so we need to + # resize the DEM to 4097x4097. + if self.map_height > 4096: + correct_dem_path = os.path.join(dem_directory, "correct_dem.png") + save_path = correct_dem_path + + output_size = self.map_height // 2 + 1 + bugged_dem_data = cv2.resize( + dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR + ) + # pylint: disable=no-member + cv2.imwrite(main_dem_path, bugged_dem_data) + else: + save_path = main_dem_path + + cv2.imwrite(save_path, resized_dem_data) # pylint: disable=no-member + self.logger.info("DEM cutout saved: %s", save_path) - return main_dem_path + return save_path # pylint: disable=too-many-locals def plane_from_np(self, tile_code: str, dem_data: np.ndarray, save_path: str) -> None: diff --git a/maps4fs/generator/i3d.py b/maps4fs/generator/i3d.py index c06c7ec..845c80f 100644 --- a/maps4fs/generator/i3d.py +++ b/maps4fs/generator/i3d.py @@ -34,6 +34,8 @@ class I3d(Component): def preprocess(self) -> None: """Gets the path to the map I3D file from the game instance and saves it to the instance attribute. If the game does not support I3D files, the attribute is set to None.""" + self.auto_process = self.kwargs.get("auto_process", False) + try: self._map_i3d_path = self.game.i3d_file_path(self.map_directory) self.logger.debug("Map I3D path: %s.", self._map_i3d_path) @@ -69,22 +71,16 @@ def _update_i3d_file(self) -> None: root = tree.getroot() for map_elem in root.iter("Scene"): for terrain_elem in map_elem.iter("TerrainTransformGroup"): - terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE)) - self.logger.debug( - "heightScale attribute set to %s in TerrainTransformGroup element.", - DEFAULT_HEIGHT_SCALE, - ) - terrain_elem.set("maxLODDistance", str(DEFAULT_MAX_LOD_DISTANCE)) - self.logger.debug( - "maxLODDistance attribute set to %s in TerrainTransformGroup element.", - DEFAULT_MAX_LOD_DISTANCE, - ) - - terrain_elem.set("occMaxLODDistance", str(DEFAULT_MAX_LOD_OCCLUDER_DISTANCE)) - self.logger.debug( - "occMaxLODDistance attribute set to %s in TerrainTransformGroup element.", - DEFAULT_MAX_LOD_OCCLUDER_DISTANCE, - ) + if self.auto_process: + terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE)) + self.logger.debug( + "heightScale attribute set to %s in TerrainTransformGroup element.", + DEFAULT_HEIGHT_SCALE, + ) + else: + self.logger.debug( + "Auto process is disabled, skipping the heightScale attribute update." + ) self.logger.debug("TerrainTransformGroup element updated in I3D file.") diff --git a/maps4fs/generator/texture.py b/maps4fs/generator/texture.py index 524c855..c09ad1b 100644 --- a/maps4fs/generator/texture.py +++ b/maps4fs/generator/texture.py @@ -159,6 +159,7 @@ def paths(self, weights_directory: str) -> list[str]: def preprocess(self) -> None: self.light_version = self.kwargs.get("light_version", False) + self.fields_padding = self.kwargs.get("fields_padding", 0) self.logger.debug("Light version: %s.", self.light_version) if not os.path.isfile(self.game.texture_schema): @@ -476,7 +477,9 @@ def _to_np(self, geometry: shapely.geometry.polygon.Polygon, *args) -> np.ndarra pairs = list(zip(xs, ys)) return np.array(pairs, dtype=np.int32).reshape((-1, 1, 2)) - def _to_polygon(self, obj: pd.core.series.Series, width: int | None) -> np.ndarray | None: + def _to_polygon( + self, obj: pd.core.series.Series, width: int | None + ) -> shapely.geometry.polygon.Polygon: """Converts OSM object to numpy array of polygon points. Arguments: @@ -484,7 +487,7 @@ def _to_polygon(self, obj: pd.core.series.Series, width: int | None) -> np.ndarr width (int | None): Width of the polygon in meters. Returns: - np.ndarray | None: Numpy array of polygon points. + shapely.geometry.polygon.Polygon: Polygon geometry. """ geometry = obj["geometry"] geometry_type = geometry.geom_type @@ -498,7 +501,7 @@ def _sequence( self, geometry: shapely.geometry.linestring.LineString | shapely.geometry.point.Point, width: int | None, - ) -> np.ndarray: + ) -> shapely.geometry.polygon.Polygon: """Converts LineString or Point geometry to numpy array of polygon points. Arguments: @@ -507,10 +510,23 @@ def _sequence( width (int | None): Width of the polygon in meters. Returns: - np.ndarray: Numpy array of polygon points. + shapely.geometry.polygon.Polygon: Polygon geometry. """ polygon = geometry.buffer(width) - return self._to_np(polygon) + return polygon + + def _skip( + self, geometry: shapely.geometry.polygon.Polygon, *args, **kwargs + ) -> shapely.geometry.polygon.Polygon: + """Returns the same geometry. + + Arguments: + geometry (shapely.geometry.polygon.Polygon): Polygon geometry. + + Returns: + shapely.geometry.polygon.Polygon: Polygon geometry. + """ + return geometry def _converters( self, geom_type: str @@ -523,7 +539,7 @@ def _converters( Returns: Callable[[shapely.geometry, int | None], np.ndarray]: Converter function. """ - converters = {"Polygon": self._to_np, "LineString": self._sequence, "Point": self._sequence} + converters = {"Polygon": self._skip, "LineString": self._sequence, "Point": self._sequence} return converters.get(geom_type) # type: ignore def polygons( @@ -538,6 +554,7 @@ def polygons( Yields: Generator[np.ndarray, None, None]: Numpy array of polygon points. """ + is_fieds = "farmland" in tags.values() try: objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags) except Exception as e: # pylint: disable=W0718 @@ -551,7 +568,17 @@ def polygons( polygon = self._to_polygon(obj, width) if polygon is None: continue - yield polygon + + if is_fieds and self.fields_padding > 0: + padded_polygon = polygon.buffer(-self.fields_padding) + + if not isinstance(padded_polygon, shapely.geometry.polygon.Polygon): + self.logger.warning("The padding value is too high, field will not padded.") + else: + polygon = padded_polygon + + polygon_np = self._to_np(polygon) + yield polygon_np def previews(self) -> list[str]: """Invokes methods to generate previews. Returns list of paths to previews. diff --git a/webui/generator.py b/webui/generator.py index 29ddf3b..e25d881 100644 --- a/webui/generator.py +++ b/webui/generator.py @@ -19,6 +19,7 @@ DEFAULT_LAT = 45.28571409289627 DEFAULT_LON = 20.237433441210115 +Image.MAX_IMAGE_PIXELS = None class GeneratorUI: @@ -190,6 +191,7 @@ def add_left_widgets(self) -> None: self.blur_radius_input = DEFAULT_BLUR_RADIUS self.plateau_height_input = DEFAULT_PLATEAU self.only_full_tiles = True + self.fields_padding = 0 if not self.auto_process: self.logger.info("Auto preset is disabled.") @@ -213,62 +215,80 @@ def add_left_widgets(self) -> None: self.logger.debug("Advanced settings are enabled.") st.warning("⚠️ Changing these settings can lead to unexpected results.") - st.info( - "ℹ️ [DEM] is for settings related to the Digital Elevation Model (elevation map). " - "This file is used to generate the terrain of the map (hills, valleys, etc.)." - ) - # Show multiplier and blur radius inputs. - st.write("[DEM] Enter multiplier for the elevation map:") - st.write(Messages.DEM_MULTIPLIER_INFO) - - self.multiplier_input = st.number_input( - "Multiplier", - value=DEFAULT_MULTIPLIER, - min_value=0, - max_value=10000, - step=1, - key="multiplier", - label_visibility="collapsed", - ) - st.write("[DEM] Enter blur radius for the elevation map:") - st.write(Messages.DEM_BLUR_RADIUS_INFO) - - self.blur_radius_input = st.number_input( - "Blur Radius", - value=DEFAULT_BLUR_RADIUS, - min_value=0, - max_value=300, - key="blur_radius", - label_visibility="collapsed", - step=2, - ) + with st.expander("DEM Advanced Settings", icon="⛰️"): + st.info( + "ℹ️ Settings related to the Digital Elevation Model (elevation map). " + "This file is used to generate the terrain of the map (hills, valleys, etc.)." + ) + # Show multiplier and blur radius inputs. + st.write("Enter the multiplier for the elevation map:") + st.write(Messages.DEM_MULTIPLIER_INFO) + + self.multiplier_input = st.number_input( + "Multiplier", + value=DEFAULT_MULTIPLIER, + min_value=0, + max_value=10000, + step=1, + key="multiplier", + label_visibility="collapsed", + ) - st.write("[DEM] Enter the plateau height (which will be added to the whole map):") - st.write(Messages.DEM_PLATEAU_INFO) - self.plateau_height_input = st.number_input( - "Plateau Height", - value=0, - min_value=0, - max_value=10000, - key="plateau_height", - label_visibility="collapsed", - ) + st.write("Enter the blur radius for the elevation map:") + st.write(Messages.DEM_BLUR_RADIUS_INFO) + + self.blur_radius_input = st.number_input( + "Blur Radius", + value=DEFAULT_BLUR_RADIUS, + min_value=0, + max_value=300, + key="blur_radius", + label_visibility="collapsed", + step=2, + ) - st.info( - "ℹ️ [Background Terrain] is for settings related to the background terrain " - "which is a simple mesh around the playable area. " - ) + st.write("Enter the plateau height (which will be added to the whole map):") + st.write(Messages.DEM_PLATEAU_INFO) + self.plateau_height_input = st.number_input( + "Plateau Height", + value=0, + min_value=0, + max_value=10000, + key="plateau_height", + label_visibility="collapsed", + ) - st.write( - "[Background Terrain] Generate only full tiles (recommended) or all tiles:" - ) - st.write(Messages.ONLY_FULL_TILES_INFO) - self.only_full_tiles = st.checkbox( - "Only Full Background Tiles", - key="only_full_tiles", - value=True, - ) + with st.expander("Background Terrain Advanced Settings", icon="🏞️"): + st.info( + "ℹ️ Settings related to the background terrain " + "which is a simple mesh around the playable area. " + ) + + st.write("Generate only full tiles (recommended) or all tiles:") + st.write(Messages.ONLY_FULL_TILES_INFO) + self.only_full_tiles = st.checkbox( + "Only Full Background Tiles", + key="only_full_tiles", + value=True, + ) + + with st.expander("Textures Advanced Settings", icon="🎨"): + st.info( + "ℹ️ Settings related to the textures of the map, which represent different " + "types of terrain, such as grass, dirt, etc." + ) + + st.write("Enter the field padding (in meters):") + st.write(Messages.FIELD_PADDING_INFO) + self.fields_padding = st.number_input( + "Field Padding", + value=0, + min_value=0, + max_value=100, + key="field_padding", + label_visibility="collapsed", + ) # Add an empty container for status messages. self.status_container = st.empty() @@ -374,6 +394,7 @@ def generate_map(self) -> None: plateau=self.plateau_height_input, light_version=self.community, only_full_tiles=self.only_full_tiles, + fields_padding=self.fields_padding, ) if self.community: diff --git a/webui/templates.py b/webui/templates.py index c2f7e45..ead684e 100644 --- a/webui/templates.py +++ b/webui/templates.py @@ -1,7 +1,9 @@ class Messages: TITLE = "maps4FS" MAIN_PAGE_DESCRIPTION = ( - "Generate map templates for Farming Simulator from real places. \n" + "Generate map templates for Farming Simulator from real places. \n\n" + "If some objects (buidings, fields, etc.) are missing or misplaced, \nyou can edit them " + "by yourself on the 🌎 [OpenStreetMap](https://www.openstreetmap.org/) website. \n\n" "💬 Join our [Discord server](https://discord.gg/Sj5QKKyE42) to get help, share your " "maps, or just chat. \n" "🤗 If you like the project, consider supporting it on [Buy Me a Coffee](https://www.buymeacoffee.com/iwatkot). \n" @@ -84,3 +86,7 @@ class Messages: "In most cases you don't need splitted tiles, so it's recommended to keep this option " "checked." ) + FIELD_PADDING_INFO = ( + "This value is used to add some padding around the fields. " + "It will make the fields smaller, can be useful if they are too close to each other." + )