From 116428f4c8f6fb57db9cdc18d3dc303cced91d8d Mon Sep 17 00:00:00 2001 From: Stan Soldatov Date: Sun, 22 Dec 2024 04:03:35 +0100 Subject: [PATCH] Generation of forests. --- data/fs25-map-template.zip | Bin 2196054 -> 2196770 bytes data/fs25-tree-schema.json | 338 +++++++++++++++++++++++++++++++++++ maps4fs/generator/game.py | 15 ++ maps4fs/generator/grle.py | 2 + maps4fs/generator/i3d.py | 119 +++++++++++- maps4fs/generator/texture.py | 1 - 6 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 data/fs25-tree-schema.json diff --git a/data/fs25-map-template.zip b/data/fs25-map-template.zip index 16def5ed317d509925e40cfb32cd9c973b471d85..1c156d09b1bf1e5a62697387a9bffe53fcf93ac6 100644 GIT binary patch delta 15420 zcmZ|0bxa;z)b5SDyA~<##ogWA-6>9S8z}A$1&X^AcX#*V?k>e0zWaIe{qdfYoMiuU z?X|A8=H4@z%q08HECz>y=SPNu*I)tw7)h#*Lg8}BFfSO`IxPekA{ZE$y@?Yu2qqgg z^Gbs?mos)Ozax5HVndsPsEc1atir9xM8?)8c!pC?6oO@BF+mqi$<#^T2 z?0>WwzAIgj2)RsXeJ=hT2c`vETAuc!+NLre#teY>tGddJC<~3L^7Ea)A9s4Q&+1}EcZ*b{DHe+>b(iQBe!TMxhMO+Q~D`zyQ{Cavzpl$#xP z+d5kEJXZQbe`0Nv~jPlkKpg53A3jbUD|*sCkxf=zPpkP`?btMxOhEz2Kg*G zx`om?o#oG0fni+_q6{kh7qS%;&S0j+?P?jv=a6|p!?Y3)Rrioq z$S*plzi)hqo$`G_m(~?mg-S$EU=K8xpb#UKAWy9PV^Ql_>1cGQrBwsijzlG-o6NC zc8-^Kchge=X$#{Yj&|xv;-Hl@d;fQ&DC+;FO_jIBzi?M@QR6haN~7L$azEybeQS}2 z3XFZf?p7H4((lS;I0)5CY%rxiXn9-;g@-5lw@*8?{&?Rz2Ud@=)%7~uulEEqxxU^% z@Gx6GPA<57g!Bz`{ zn!ldUvb?qd1djECShP~f|QBiDQ0S5 zN4{S3=ws|a{DhCIuh?2TJF{cm!P?`3+3bAZ#h7gKboq_*H*5#4ljpT=+M|#&W{PBK z(bnl{T79|ZC54mXvBI?!*ViA}N+FB9d5pM!58cjy-0uUC8p#N7;m{SHrAR`DE0`3N z*d-89gaRfsf(guRO%;gu{C<{(A>g@eG@@oTYBoux}0l)(4oYzZEt z)H^#Bz=5ui{bZ$YU0vD>Bagp%ibwPdxy-^>wpkP^>B@aG>2-?F9>Jf&Ce_>q(e z7*h@(x`KbY75%NJ5!RU(*4a#szg4CqZX>xNEjgYgeaGS;$jY=1Z{cA0x1*4ZKx&Nq z%e<80OdG=dVES?s0%rmh{-|@$;<1;m3x*d*5ztpev(d@$qYO$Dj0Wd#UFD|v{T7BO z1t~*aiD{E;?`Qe%>8Mo$X=O9;?>rth@AiJ8(zGFW_FW?tm7^sX<_yA|gBWz05T;Sb#b086PYY3GD6WV$DuTz(?!O3Ix?smio>q#2oHez^-7~av?=`(z{M3S62HeEV!ePmvN zD3>kXXse*+DqJe5TYhsi)#h3p$e86{9Rs>7(&N#p41x>hLo_)yDf& zbHow3#-GhYWYGY8wRjI#nB}#h;0I}UbM^twturi4( zrd~wb3OZ<~skGtKJ+z7;9JxxxSjWLssreNkh@lgf*fklQsrpMmjxH*JrM%p(jTzpI!ZQv7`aRFK@6x>5F@3JW=E3?sFJ}B!{A4L*#e>@n{UR-- zDTDcqeABI>K4fX+=zjHyX}&doVC4*CQjCb{Cf8plvLvJ<^iFVK+>ad#f;TZ9zm5z|(jwek7ezqF*v?$cr!d$>8bs zS=SYxcNMmw=`LMJ8v)Xf)Hm-!sZr^K?ATH7Mi}{|0jtFKPM=RT +WiOPnXJm?Nx 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

pE_BF(aSEpCX(!C&wwPikbz4_(} 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 diff --git a/data/fs25-tree-schema.json b/data/fs25-tree-schema.json new file mode 100644 index 00000000..597dc55a --- /dev/null +++ b/data/fs25-tree-schema.json @@ -0,0 +1,338 @@ +[ + { + "name": "americanElmRavaged", + "reference_id": 1000 + }, + { + "name": "americanElm_stage01", + "reference_id": 1001 + }, + { + "name": "americanElm_stage02", + "reference_id": 1002 + }, + { + "name": "americanElm_stage03", + "reference_id": 1003 + }, + { + "name": "americanElm_stage04", + "reference_id": 1004 + }, + { + "name": "americanElm_stage05", + "reference_id": 1005 + }, + { + "name": "apple_stage03", + "reference_id": 1006 + }, + { + "name": "aspen_stage01", + "reference_id": 1007 + }, + { + "name": "aspen_stage02", + "reference_id": 1008 + }, + { + "name": "aspen_stage03", + "reference_id": 1009 + }, + { + "name": "aspen_stage04", + "reference_id": 1010 + }, + { + "name": "aspen_stage05", + "reference_id": 1011 + }, + { + "name": "aspen_stage06_var01", + "reference_id": 1012 + }, + { + "name": "aspen_stage06_var02", + "reference_id": 1013 + }, + { + "name": "beech_stage01", + "reference_id": 1014 + }, + { + "name": "beech_stage02", + "reference_id": 1015 + }, + { + "name": "beech_stage02_var02", + "reference_id": 1016 + }, + { + "name": "beech_stage02_var03", + "reference_id": 1017 + }, + { + "name": "beech_stage03", + "reference_id": 1018 + }, + { + "name": "beech_stage04", + "reference_id": 1019 + }, + { + "name": "beech_stage05", + "reference_id": 1020 + }, + { + "name": "beech_stage06_var01", + "reference_id": 1021 + }, + { + "name": "beech_stage06_var02", + "reference_id": 1022 + }, + { + "name": "betulaErmanii_stage01", + "reference_id": 1023 + }, + { + "name": "betulaErmanii_stage02", + "reference_id": 1024 + }, + { + "name": "betulaErmanii_stage03", + "reference_id": 1025 + }, + { + "name": "betulaErmanii_stage04", + "reference_id": 1026 + }, + { + "name": "boxelder_stage01", + "reference_id": 1027 + }, + { + "name": "boxelder_stage02", + "reference_id": 1028 + }, + { + "name": "boxelder_stage03", + "reference_id": 1029 + }, + { + "name": "cherry_stage01", + "reference_id": 1030 + }, + { + "name": "cherry_stage02", + "reference_id": 1031 + }, + { + "name": "cherry_stage03", + "reference_id": 1032 + }, + { + "name": "cherry_stage04", + "reference_id": 1033 + }, + { + "name": "chineseElm_stage01", + "reference_id": 1034 + }, + { + "name": "chineseElm_stage02", + "reference_id": 1035 + }, + { + "name": "chineseElm_stage03", + "reference_id": 1036 + }, + { + "name": "chineseElm_stage04", + "reference_id": 1037 + }, + { + "name": "deadwood", + "reference_id": 1038 + }, + { + "name": "downyServiceBerry_stage01", + "reference_id": 1039 + }, + { + "name": "downyServiceBerry_stage02", + "reference_id": 1040 + }, + { + "name": "downyServiceBerry_stage03", + "reference_id": 1041 + }, + { + "name": "goldenRain_stage01", + "reference_id": 1042 + }, + { + "name": "goldenRain_stage02", + "reference_id": 1043 + }, + { + "name": "goldenRain_stage03", + "reference_id": 1044 + }, + { + "name": "goldenRain_stage04", + "reference_id": 1045 + }, + { + "name": "japaneseZelkova_stage01", + "reference_id": 1046 + }, + { + "name": "japaneseZelkova_stage02", + "reference_id": 1047 + }, + { + "name": "japaneseZelkova_stage03", + "reference_id": 1048 + }, + { + "name": "japaneseZelkova_stage04", + "reference_id": 1049 + }, + { + "name": "lodgepolePine_stage01", + "reference_id": 1050 + }, + { + "name": "lodgepolePine_stage02", + "reference_id": 1051 + }, + { + "name": "lodgepolePine_stage02Var2", + "reference_id": 1052 + }, + { + "name": "lodgepolePine_stage03", + "reference_id": 1053 + }, + { + "name": "lodgepolePine_stage03Var2", + "reference_id": 1054 + }, + { + "name": "northernCatalpa_stage01", + "reference_id": 1055 + }, + { + "name": "northernCatalpa_stage02", + "reference_id": 1056 + }, + { + "name": "northernCatalpa_stage03", + "reference_id": 1057 + }, + { + "name": "northernCatalpa_stage04", + "reference_id": 1058 + }, + { + "name": "oak_stage01", + "reference_id": 1059 + }, + { + "name": "oak_stage02", + "reference_id": 1060 + }, + { + "name": "oak_stage03", + "reference_id": 1061 + }, + { + "name": "oak_stage04", + "reference_id": 1062 + }, + { + "name": "oak_stage05", + "reference_id": 1063 + }, + { + "name": "pinusSylvestris_stage01", + "reference_id": 1064 + }, + { + "name": "pinusSylvestris_stage02", + "reference_id": 1065 + }, + { + "name": "pinusSylvestris_stage03", + "reference_id": 1066 + }, + { + "name": "pinusSylvestris_stage04", + "reference_id": 1067 + }, + { + "name": "pinusSylvestris_stage05", + "reference_id": 1068 + }, + { + "name": "pinusTabuliformis_stage01", + "reference_id": 1069 + }, + { + "name": "pinusTabuliformis_stage02", + "reference_id": 1070 + }, + { + "name": "pinusTabuliformis_stage03", + "reference_id": 1071 + }, + { + "name": "pinusTabuliformis_stage04", + "reference_id": 1072 + }, + { + "name": "pinusTabuliformis_stage05", + "reference_id": 1073 + }, + { + "name": "shagbarkHickory_stage01", + "reference_id": 1074 + }, + { + "name": "shagbarkHickory_stage02", + "reference_id": 1075 + }, + { + "name": "shagbarkHickory_stage03", + "reference_id": 1076 + }, + { + "name": "shagbarkHickory_stage04", + "reference_id": 1077 + }, + { + "name": "tiliaAmurensis_stage01", + "reference_id": 1078 + }, + { + "name": "tiliaAmurensis_stage02", + "reference_id": 1079 + }, + { + "name": "tiliaAmurensis_stage03", + "reference_id": 1080 + }, + { + "name": "tiliaAmurensis_stage04", + "reference_id": 1081 + }, + { + "name": "transportTree", + "reference_id": 1082 + }, + { + "name": "treesRavaged", + "reference_id": 1083 + } +] diff --git a/maps4fs/generator/game.py b/maps4fs/generator/game.py index e576a833..c4f362b2 100644 --- a/maps4fs/generator/game.py +++ b/maps4fs/generator/game.py @@ -36,6 +36,7 @@ class Game: _map_template_path: str | None = None _texture_schema: str | None = None _grle_schema: str | None = None + _tree_schema: str | None = None # Order matters! Some components depend on others. components = [Texture, I3d, GRLE, Background, Config] @@ -109,6 +110,19 @@ def grle_schema(self) -> str: raise ValueError("GRLE layers schema path not set.") return self._grle_schema + @property + def tree_schema(self) -> str: + """Returns the path to the tree layers schema file. + + Raises: + ValueError: If the tree layers schema path is not set. + + Returns: + str: The path to the tree layers schema file.""" + if not self._tree_schema: + raise ValueError("Tree layers schema path not set.") + return self._tree_schema + def dem_file_path(self, map_directory: str) -> str: """Returns the path to the DEM file. @@ -187,6 +201,7 @@ class FS25(Game): _map_template_path = os.path.join(working_directory, "data", "fs25-map-template.zip") _texture_schema = os.path.join(working_directory, "data", "fs25-texture-schema.json") _grle_schema = os.path.join(working_directory, "data", "fs25-grle-schema.json") + _tree_schema = os.path.join(working_directory, "data", "fs25-tree-schema.json") def dem_file_path(self, map_directory: str) -> str: """Returns the path to the DEM file. diff --git a/maps4fs/generator/grle.py b/maps4fs/generator/grle.py index c2c11fbe..2b0e61e3 100644 --- a/maps4fs/generator/grle.py +++ b/maps4fs/generator/grle.py @@ -218,6 +218,8 @@ def _add_plants(self) -> None: grass_image_path = grass_layer.get_preview_or_path(weights_directory) self.logger.debug("Grass image path: %s.", grass_image_path) + # TODO: Get the forest layer and combine it with the grass layer. + if not grass_image_path or not os.path.isfile(grass_image_path): self.logger.warning("Base image not found in %s.", grass_image_path) return diff --git a/maps4fs/generator/i3d.py b/maps4fs/generator/i3d.py index 2bb750af..af2821aa 100644 --- a/maps4fs/generator/i3d.py +++ b/maps4fs/generator/i3d.py @@ -4,15 +4,22 @@ import json import os +from random import choice +from typing import Generator from xml.etree import ElementTree as ET +import cv2 +import numpy as np + from maps4fs.generator.component import Component +from maps4fs.generator.texture import Texture DEFAULT_HEIGHT_SCALE = 2000 DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768 DEFAULT_MAX_LOD_DISTANCE = 10000 DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000 -NODE_ID_STARTING_VALUE = 500 +NODE_ID_STARTING_VALUE = 2000 +TREE_NODE_ID_STARTING_VALUE = 4000 # pylint: disable=R0903 @@ -48,6 +55,7 @@ def process(self) -> None: """Updates the map I3D file with the default settings.""" self._update_i3d_file() self._add_fields() + self._add_forests() def _get_tree(self) -> ET.ElementTree | None: """Returns the ElementTree instance of the map I3D file.""" @@ -316,3 +324,112 @@ def create_attribute_node(name: str, attr_type: str, value: str) -> ET.Element: attribute_node.set("type", attr_type) attribute_node.set("value", value) return attribute_node + + def _add_forests(self) -> None: + try: + tree_schema_path = self.game.tree_schema + except ValueError: + self.logger.warning("Tree schema path not set for the Game %s.", self.game.code) + return + + if not os.path.isfile(tree_schema_path): + self.logger.warning("Tree schema file was not found: %s.", tree_schema_path) + return + + try: + with open(tree_schema_path, "r", encoding="utf-8") as tree_schema_file: + tree_schema: list[dict[str, str | int]] = json.load(tree_schema_file) + except json.JSONDecodeError as e: + self.logger.warning( + "Could not load tree schema from %s with error: %s", tree_schema_path, e + ) + return + + texture_component: Texture | None = self.map.get_component("Texture") + if not texture_component: + self.logger.warning("Texture component not found.") + return + + forest_layer = texture_component.get_layer_by_usage("forest") + + if not forest_layer: + self.logger.warning("Forest layer not found.") + return + + weights_directory = self.game.weights_dir_path(self.map_directory) + forest_image_path = forest_layer.get_preview_or_path(weights_directory) + + if not forest_image_path or not os.path.isfile(forest_image_path): + self.logger.warning("Forest image not found.") + return + + tree = self._get_tree() + if tree is None: + return + + # Find the element in the I3D file. + root = tree.getroot() + scene_node = root.find(".//Scene") + if scene_node is None: + self.logger.warning("Scene element not found in I3D file.") + return + + self.logger.debug("Scene element found in I3D file, starting to add forests.") + + node_id = TREE_NODE_ID_STARTING_VALUE + + # Create element. + trees_node = ET.Element("TransformGroup") + trees_node.set("name", "trees") + trees_node.set("translation", "0 400 0") + trees_node.set("nodeId", str(node_id)) + node_id += 1 + + forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED) + + tree_count = 0 + for x, y in self.non_empty_pixels(forest_image, step=2): + xcs, ycs = self.top_left_coordinates_to_center((x, y)) + node_id += 1 + + # TODO: Randomize coordinates. + + random_tree = choice(tree_schema) + tree_name = random_tree["name"] + tree_id = random_tree["reference_id"] + + # + reference_node = ET.Element("ReferenceNode") + reference_node.set("name", tree_name) + reference_node.set("translation", f"{xcs} 0 {ycs}") + reference_node.set("rotation", "0 -0 0") + reference_node.set("referenceId", str(tree_id)) + reference_node.set("nodeId", str(node_id)) + + trees_node.append(reference_node) + tree_count += 1 + + scene_node.append(trees_node) + self.logger.info("Added %s trees to the I3D file.", tree_count) + + tree.write(self._map_i3d_path) # type: ignore + self.logger.info("Map I3D file saved to: %s.", self._map_i3d_path) + + @staticmethod + def non_empty_pixels( + image: np.ndarray, step: int = 1 + ) -> Generator[tuple[int, int], None, None]: + """Receives numpy array, which represents single-channeled image of uint8 type. + Yield coordinates of non-empty pixels (pixels with value greater than 0). + + Arguments: + image (np.ndarray): The image to get non-empty pixels from. + step (int, optional): The step to iterate through the image. Defaults to 1. + + Yields: + tuple[int, int]: The coordinates of non-empty pixels. + """ + for y, row in enumerate(image[::step]): + for x, value in enumerate(row[::step]): + if value > 0: + yield x * step, y * step diff --git a/maps4fs/generator/texture.py b/maps4fs/generator/texture.py index 9f7e5798..99351072 100644 --- a/maps4fs/generator/texture.py +++ b/maps4fs/generator/texture.py @@ -5,7 +5,6 @@ import json import os import re -import shutil from collections import defaultdict from typing import Any, Callable, Generator, Optional