Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Place trees on terrain #202

Merged
merged 3 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,8 @@ You can also apply some advanced settings to the map generation process.<br>

- Plants island percent - defines the relation between the map size and the number of islands of plants. For example, if set to 100% for map size of 2048 will be added 2048 islands of plants.

- Fill empty farmlands - if enabled, the empty (zero value) pixels of the farmlands image will be replaces with the value of 255.

### I3D Advanced settings

- Forest density - the density of the forest in meters. The lower the value, the lower the distance between the trees, which makes the forest denser. Note, that low values will lead to enormous number of trees, which may cause the Giants Editor to crash or lead to performance issues. By default, it's set to 10.
Expand Down
13 changes: 13 additions & 0 deletions maps4fs/generator/component/grle.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from maps4fs.generator.component.base.component_xml import XMLComponent
from maps4fs.generator.settings import Parameters

FARMLAND_ID_LIMIT = 254


def plant_to_pixel_value(plant_name: str) -> int | None:
"""Returns the pixel value representation of the plant.
Expand Down Expand Up @@ -225,6 +227,13 @@ def _add_farmlands(self) -> None:

farmland_np = self.polygon_points_to_np(fitted_farmland, divide=2)

if farmland_id > FARMLAND_ID_LIMIT:
self.logger.warning(
"Farmland ID limit reached. Skipping the rest of the farmlands. "
"Giants Editor supports maximum 254 farmlands."
)
break

try:
cv2.fillPoly(image, [farmland_np], (float(farmland_id),))
except Exception as e:
Expand All @@ -246,6 +255,10 @@ def _add_farmlands(self) -> None:

self.save_tree(tree)

# Replace all the zero values on the info layer image with 255.
if self.map.grle_settings.fill_empty_farmlands:
image[image == 0] = 255

cv2.imwrite(info_layer_farmlands_path, image)

self.preview_paths["farmlands"] = info_layer_farmlands_path
Expand Down
82 changes: 60 additions & 22 deletions maps4fs/generator/component/i3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
NODE_ID_STARTING_VALUE = 2000
SPLINES_NODE_ID_STARTING_VALUE = 5000
TREE_NODE_ID_STARTING_VALUE = 10000
TREES_DEFAULT_Z_VALUE = 400

FIELDS_ATTRIBUTES = [
("angle", "integer", "0"),
Expand Down Expand Up @@ -132,17 +131,10 @@ def _add_splines(self) -> None:
self.logger.warning("Shapes or Scene node not found in I3D file.")
return

# Read the not resized DEM to obtain Z values for spline points.
background_component = self.map.get_background_component()
if not background_component:
self.logger.warning("Background component not found.")
return

not_resized_dem = cv2.imread(background_component.not_resized_path, cv2.IMREAD_UNCHANGED)
not_resized_dem = self.get_not_resized_dem()
if not_resized_dem is None:
self.logger.warning("Not resized DEM not found.")
return
dem_x_size, dem_y_size = not_resized_dem.shape

user_attributes_node = root.find(".//UserAttributes")
if user_attributes_node is None:
Expand Down Expand Up @@ -201,11 +193,7 @@ def _add_splines(self) -> None:
cx, cy = point_ccs
x, y = point

x = max(0, min(int(x), dem_x_size - 1))
y = max(0, min(int(y), dem_y_size - 1))

z = not_resized_dem[y, x]
z *= self.get_z_scaling_factor()
z = self.get_z_coordinate_from_dem(not_resized_dem, x, y)

nurbs_curve_node.append(self.create_element("cv", {"c": f"{cx}, {z}, {cy}"}))

Expand Down Expand Up @@ -471,31 +459,41 @@ def _add_forests(self) -> None:
"TransformGroup",
{
"name": "trees",
"translation": f"0 {TREES_DEFAULT_Z_VALUE} 0",
"translation": "0 0 0",
"nodeId": str(node_id),
},
)
node_id += 1

not_resized_dem = self.get_not_resized_dem()
if not_resized_dem is None:
self.logger.warning("Not resized DEM not found.")
return

forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
for x, y in self.non_empty_pixels(forest_image, step=self.map.i3d_settings.forest_density):
xcs, ycs = self.top_left_coordinates_to_center((x, y))
node_id += 1

rotation = randint(-180, 180)
shifted_xcs, shifted_ycs = self.randomize_coordinates(
(xcs, ycs),
shifted_x, shifted_y = self.randomize_coordinates(
(x, y),
self.map.i3d_settings.forest_density,
self.map.i3d_settings.trees_relative_shift,
)

shifted_x, shifted_y = int(shifted_x), int(shifted_y)

z = self.get_z_coordinate_from_dem(not_resized_dem, shifted_x, shifted_y)

xcs, ycs = self.top_left_coordinates_to_center((shifted_x, shifted_y))
node_id += 1

rotation = randint(-180, 180)

random_tree = choice(tree_schema)
tree_name = random_tree["name"]
tree_id = random_tree["reference_id"]

data = {
"name": tree_name,
"translation": f"{shifted_xcs} 0 {shifted_ycs}",
"translation": f"{xcs} {z} {ycs}",
"rotation": f"0 {rotation} 0",
"referenceId": str(tree_id),
"nodeId": str(node_id),
Expand Down Expand Up @@ -546,3 +544,43 @@ def non_empty_pixels(
for x, value in enumerate(row[::step]):
if value > 0:
yield x * step, y * step

def get_not_resized_dem(self) -> np.ndarray | None:
"""Reads the not resized DEM image from the background component.

Returns:
np.ndarray | None: The not resized DEM image or None if the image could not be read.
"""
background_component = self.map.get_background_component()
if not background_component:
self.logger.warning("Background component not found.")
return None

if not background_component.not_resized_path:
self.logger.warning("Not resized DEM path not found.")
return None

not_resized_dem = cv2.imread(background_component.not_resized_path, cv2.IMREAD_UNCHANGED)

return not_resized_dem

def get_z_coordinate_from_dem(self, not_resized_dem: np.ndarray, x: int, y: int) -> float:
"""Gets the Z coordinate from the DEM image for the given coordinates.

Arguments:
not_resized_dem (np.ndarray): The not resized DEM image.
x (int): The x coordinate.
y (int): The y coordinate.

Returns:
float: The Z coordinate.
"""
dem_x_size, dem_y_size = not_resized_dem.shape

x = int(max(0, min(x, dem_x_size - 1)))
y = int(max(0, min(y, dem_y_size - 1)))

z = not_resized_dem[y, x]
z *= self.get_z_scaling_factor()

return z
1 change: 1 addition & 0 deletions maps4fs/generator/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ class GRLESettings(SettingsModel):
plants_island_vertex_count: int = 30
plants_island_rounding_radius: int = 15
plants_island_percent: int = 100
fill_empty_farmlands: bool = False


class I3DSettings(SettingsModel):
Expand Down
4 changes: 4 additions & 0 deletions webui/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ class Settings:
"expensive, make this value higher than 100. To make it cheaper, make it lower than 100. \n"
"ℹ️ **Units:** percents of the base price."
)
FILL_EMPTY_FARMLANDS = (
"If fill empty farmlands is enabled, the empty (zero value) pixels of the farmlands "
"info layer image will be filled with 255 value."
)

# I3D Settings

Expand Down