Skip to content

Commit

Permalink
Generating deco foliage.
Browse files Browse the repository at this point in the history
  • Loading branch information
iwatkot committed Dec 21, 2024
1 parent 5bb261d commit 845437a
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 2 deletions.
11 changes: 9 additions & 2 deletions data/fs25-texture-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@
},
{
"name": "mudPebblesLight",
"count": 2
"count": 2,
"tags": { "waterway": ["ditch", "drain"] },
"width": 2,
"color": [33, 67, 101]
},
{
"name": "mudTracks",
Expand Down Expand Up @@ -199,7 +202,11 @@
{
"name": "sand",
"count": 2,
"tags": { "natural": "water", "waterway": true, "landuse": "basin" },
"tags": {
"natural": "water",
"waterway": ["river", "stream", "flowline", "tidal_channel", "canal"],
"landuse": "basin"
},
"width": 10,
"color": [255, 20, 20]
}
Expand Down
15 changes: 15 additions & 0 deletions maps4fs/generator/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Game:
_map_template_path: str | None = None
_texture_schema: str | None = None
_grle_schema: str | None = None
_base_image: str | None = None

# Order matters! Some components depend on others.
components = [Texture, I3d, GRLE, Background, Config]
Expand Down Expand Up @@ -130,6 +131,19 @@ def weights_dir_path(self, map_directory: str) -> str:
str: The path to the weights directory."""
raise NotImplementedError

def base_image_path(self, map_directory: str) -> str | None:
"""Returns the path to the base density map image.
Arguments:
map_directory (str): The path to the map directory.
Returns:
str: The path to the base density map image or None if not set.
"""
if self._base_image:
return os.path.join(self.weights_dir_path(map_directory), self._base_image)
return None

def i3d_file_path(self, map_directory: str) -> str:
"""Returns the path to the i3d file.
Expand Down Expand Up @@ -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")
_base_image = "base.png"

def dem_file_path(self, map_directory: str) -> str:
"""Returns the path to the DEM file.
Expand Down
129 changes: 129 additions & 0 deletions maps4fs/generator/grle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

import json
import os
from random import choice, randint
from xml.etree import ElementTree as ET

import cv2
import numpy as np

from maps4fs.generator.component import Component

ISLAND_SIZE_MIN = 10
ISLAND_SIZE_MAX = 200


# pylint: disable=W0223
class GRLE(Component):
Expand Down Expand Up @@ -76,6 +80,11 @@ def process(self) -> None:
self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)

self._add_farmlands()
if self.game.code == "FS25":
self.logger.info("Game is %s, plants will be added.", self.game.code)
self._add_plants()
else:
self.logger.warning("Adding plants it's not supported for the %s.", self.game.code)

def previews(self) -> list[str]:
"""Returns a list of paths to the preview images (empty list).
Expand Down Expand Up @@ -184,3 +193,123 @@ def _add_farmlands(self) -> None:
self.logger.info(
"Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
)

def _add_plants(self) -> None:
"""Adds plants to the InfoLayer PNG file."""
# 1. Get the path to the densityMap_fruits.png.
# 2. Get the path to the base layer (grass).
# 3. Detect non-zero areas in the base layer (it's where the plants will be placed).
base_image_path = self.game.base_image_path(self.map_directory)
if not base_image_path or not os.path.isfile(base_image_path):
self.logger.warning("Base image not found in %s.", base_image_path)
return

density_map_fruit_path = os.path.join(
self.game.weights_dir_path(self.map_directory), "densityMap_fruits.png"
)

if not os.path.isfile(density_map_fruit_path):
self.logger.warning("Density map for fruits not found in %s.", density_map_fruit_path)
return

# Single channeled 8-bit image, where non-zero values (255) are where the grass is.
base_image = cv2.imread(base_image_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member

# Density map of the fruits is 2X size of the base image, so we need to resize it.
# We'll resize the base image to make it bigger, so we can compare the values.
base_image = cv2.resize( # pylint: disable=no-member
base_image,
(base_image.shape[1] * 2, base_image.shape[0] * 2),
interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member
)

# B and G channels remain the same (zeros), while we change the R channel.
possible_R_values = [33, 65, 97, 129, 161, 193, 225] # pylint: disable=C0103

# 1st approach: Change the non zero values in the base image to 33 (for debug).
# And use the base image as R channel in the density map.

# pylint: disable=no-member
def create_island_of_plants(image: np.ndarray, count: int) -> np.ndarray:
"""Create an island of plants in the image.
Arguments:
image (np.ndarray): The image where the island of plants will be created.
count (int): The number of islands of plants to create.
Returns:
np.ndarray: The image with the islands of plants.
"""
for _ in range(count):
# Randomly choose the value for the island.
plant_value = choice(possible_R_values)
# Randomly choose the size of the island.
island_size = randint(ISLAND_SIZE_MIN, ISLAND_SIZE_MAX)
# Randomly choose the position of the island.
# x = np.random.randint(0, image.shape[1] - island_size)
# y = np.random.randint(0, image.shape[0] - island_size)
x = randint(0, image.shape[1] - island_size)
y = randint(0, image.shape[0] - island_size)

# Randomly choose the shape of the island.
shapes = ["circle", "ellipse", "polygon"]
shape = choice(shapes)

try:
if shape == "circle":
center = (x + island_size // 2, y + island_size // 2)
radius = island_size // 2
cv2.circle(image, center, radius, plant_value, -1) # type: ignore
elif shape == "ellipse":
center = (x + island_size // 2, y + island_size // 2)
axes = (island_size // 2, island_size // 4)
angle = 0
cv2.ellipse( # type: ignore
image, center, axes, angle, 0, 360, plant_value, -1
)
elif shape == "polygon":
nodes_count = randint(20, 50)
nodes = []
for _ in range(nodes_count):
node = (randint(x, x + island_size), randint(y, y + island_size))
nodes.append(node)
nodes = np.array(nodes, np.int32) # type: ignore
cv2.fillPoly(image, [nodes], plant_value) # type: ignore
except Exception: # pylint: disable=W0703
continue

return image

updated_base_image = base_image.copy()
# Set all the non-zero values to 33.
updated_base_image[base_image != 0] = 33

# Add islands of plants to the base image.
island_count = self.map_size
self.logger.info("Adding %s islands of plants to the base image.", island_count)
updated_base_image = create_island_of_plants(updated_base_image, island_count)
self.logger.debug("Islands of plants added to the base image.")

# Remove the values where the base image has zeros.
updated_base_image[base_image == 0] = 0
self.logger.debug("Removed the values where the base image has zeros.")

# Value of 33 represents the base grass plant.
# After painting it with base grass, we'll create multiple islands of different plants.
# On the final step, we'll remove all the values which in pixels
# where zerons in the original base image (so we don't paint grass where it should not be).

# Three channeled 8-bit image, where non-zero values are the
# different types of plants (only in the R channel).
density_map_fruits = cv2.imread(density_map_fruit_path, cv2.IMREAD_UNCHANGED)
self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape)

# Put the updated base image as the B channel in the density map.
density_map_fruits[:, :, 0] = updated_base_image
self.logger.debug("Updated base image added as the B channel in the density map.")

# Save the updated density map.
# Ensure that order of channels is correct because CV2 uses BGR and we need RGB.
density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB)
cv2.imwrite(density_map_fruit_path, density_map_fruits)
self.logger.info("Updated density map for fruits saved in %s.", density_map_fruit_path)
8 changes: 8 additions & 0 deletions maps4fs/generator/texture.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,14 @@ def draw_base_layer(self, cumulative_image: np.ndarray) -> None:
cv2.imwrite(layer_path, img)
self.logger.info("Base texture %s saved.", layer_path)

base_path = self.game.base_image_path(self.map_directory)
if base_path:
self.logger.debug(
"Copying base layer to use it later for density map to %s.", base_path
)
# Make a copy of a base layer to the fruits density map.
cv2.imwrite(base_path, img)

def get_relative_x(self, x: float) -> int:
"""Converts UTM X coordinate to relative X coordinate in map image.
Expand Down

0 comments on commit 845437a

Please sign in to comment.