maps4fs 2.8.2__py3-none-any.whl → 2.8.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of maps4fs might be problematic. Click here for more details.
- maps4fs/generator/component/background.py +282 -1
- maps4fs/generator/component/base/component.py +23 -4
- maps4fs/generator/component/base/component_image.py +33 -0
- maps4fs/generator/component/base/component_mesh.py +407 -0
- maps4fs/generator/game.py +11 -1
- maps4fs/generator/map.py +15 -1
- maps4fs/generator/settings.py +7 -0
- {maps4fs-2.8.2.dist-info → maps4fs-2.8.3.dist-info}/METADATA +15 -11
- {maps4fs-2.8.2.dist-info → maps4fs-2.8.3.dist-info}/RECORD +12 -12
- {maps4fs-2.8.2.dist-info → maps4fs-2.8.3.dist-info}/WHEEL +0 -0
- {maps4fs-2.8.2.dist-info → maps4fs-2.8.3.dist-info}/licenses/LICENSE.md +0 -0
- {maps4fs-2.8.2.dist-info → maps4fs-2.8.3.dist-info}/top_level.txt +0 -0
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import os
|
|
7
7
|
import shutil
|
|
8
8
|
from copy import deepcopy
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, Literal
|
|
10
10
|
|
|
11
11
|
import cv2
|
|
12
12
|
import numpy as np
|
|
@@ -57,6 +57,14 @@ class Background(MeshComponent, ImageComponent):
|
|
|
57
57
|
os.makedirs(self.background_directory, exist_ok=True)
|
|
58
58
|
os.makedirs(self.water_directory, exist_ok=True)
|
|
59
59
|
|
|
60
|
+
self.textured_mesh_directory = os.path.join(self.background_directory, "textured_mesh")
|
|
61
|
+
os.makedirs(self.textured_mesh_directory, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
self.assets_background_directory = os.path.join(self.map.assets_directory, "background")
|
|
64
|
+
self.assets_water_directory = os.path.join(self.map.assets_directory, "water")
|
|
65
|
+
os.makedirs(self.assets_background_directory, exist_ok=True)
|
|
66
|
+
os.makedirs(self.assets_water_directory, exist_ok=True)
|
|
67
|
+
|
|
60
68
|
self.water_resources_path = os.path.join(self.water_directory, "water_resources.png")
|
|
61
69
|
|
|
62
70
|
self.output_path = os.path.join(self.background_directory, f"{Parameters.FULL}.png")
|
|
@@ -110,8 +118,28 @@ class Background(MeshComponent, ImageComponent):
|
|
|
110
118
|
|
|
111
119
|
if self.map.background_settings.generate_background:
|
|
112
120
|
self.generate_obj_files()
|
|
121
|
+
if self.game.mesh_processing:
|
|
122
|
+
self.logger.debug("Mesh processing is enabled, will decimate, texture and convert.")
|
|
123
|
+
self.decimate_background_mesh()
|
|
124
|
+
self.texture_background_mesh()
|
|
125
|
+
background_conversion_result = self.convert_background_mesh_to_i3d()
|
|
126
|
+
if background_conversion_result:
|
|
127
|
+
self.add_note_file(asset="background")
|
|
128
|
+
else:
|
|
129
|
+
self.logger.warning(
|
|
130
|
+
"Mesh processing is disabled for the game, skipping background mesh processing."
|
|
131
|
+
)
|
|
113
132
|
if self.map.background_settings.generate_water:
|
|
114
133
|
self.generate_water_resources_obj()
|
|
134
|
+
if self.game.mesh_processing:
|
|
135
|
+
self.logger.debug("Mesh processing is enabled, will convert water mesh to i3d.")
|
|
136
|
+
water_conversion_result = self.convert_water_mesh_to_i3d()
|
|
137
|
+
if water_conversion_result:
|
|
138
|
+
self.add_note_file(asset="water")
|
|
139
|
+
else:
|
|
140
|
+
self.logger.warning(
|
|
141
|
+
"Mesh processing is disabled for the game, skipping water mesh processing."
|
|
142
|
+
)
|
|
115
143
|
|
|
116
144
|
def create_foundations(self, dem_image: np.ndarray) -> np.ndarray:
|
|
117
145
|
"""Creates foundations for buildings based on the DEM data.
|
|
@@ -253,6 +281,257 @@ class Background(MeshComponent, ImageComponent):
|
|
|
253
281
|
remove_center=self.map.background_settings.remove_center,
|
|
254
282
|
)
|
|
255
283
|
|
|
284
|
+
@staticmethod
|
|
285
|
+
def get_decimate_factor(map_size: int) -> float:
|
|
286
|
+
"""Returns the decimation factor based on the map size.
|
|
287
|
+
|
|
288
|
+
Arguments:
|
|
289
|
+
map_size (int): The size of the map in pixels.
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
ValueError: If the map size is too large for decimation.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
float -- The decimation factor.
|
|
296
|
+
"""
|
|
297
|
+
thresholds = {
|
|
298
|
+
2048: 0.1,
|
|
299
|
+
4096: 0.05,
|
|
300
|
+
8192: 0.025,
|
|
301
|
+
16384: 0.0125,
|
|
302
|
+
}
|
|
303
|
+
for threshold, factor in thresholds.items():
|
|
304
|
+
if map_size <= threshold:
|
|
305
|
+
return factor
|
|
306
|
+
raise ValueError(
|
|
307
|
+
"Map size is too large for decimation, perform manual decimation in Blender."
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def get_background_texture_resolution(map_size: int) -> int:
|
|
312
|
+
"""Returns the background texture resolution based on the map size.
|
|
313
|
+
|
|
314
|
+
Arguments:
|
|
315
|
+
map_size (int): The size of the map in pixels.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
int -- The background texture resolution.
|
|
319
|
+
"""
|
|
320
|
+
resolutions = {
|
|
321
|
+
2048: 2048,
|
|
322
|
+
4096: Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE,
|
|
323
|
+
8192: Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE,
|
|
324
|
+
16384: Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE,
|
|
325
|
+
}
|
|
326
|
+
for threshold, resolution in resolutions.items():
|
|
327
|
+
if map_size <= threshold:
|
|
328
|
+
return resolution
|
|
329
|
+
return Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE
|
|
330
|
+
|
|
331
|
+
def decimate_background_mesh(self) -> None:
|
|
332
|
+
""" ""Decimates the background mesh based on the map size."""
|
|
333
|
+
if not self.assets.background_mesh or not os.path.isfile(self.assets.background_mesh):
|
|
334
|
+
self.logger.warning("Background mesh not found, cannot generate i3d background.")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
mesh = trimesh.load_mesh(self.assets.background_mesh, force="mesh")
|
|
339
|
+
except Exception as e:
|
|
340
|
+
self.logger.error("Could not load background mesh: %s", e)
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
decimate_factor = self.get_decimate_factor(self.map_size)
|
|
345
|
+
except ValueError as e:
|
|
346
|
+
self.logger.error("Could not determine decimation factor: %s", e)
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
decimated_save_path = os.path.join(
|
|
350
|
+
self.background_directory, f"{Parameters.DECIMATED_BACKGROUND}.obj"
|
|
351
|
+
)
|
|
352
|
+
try:
|
|
353
|
+
self.logger.debug("Decimating background mesh with factor %s.", decimate_factor)
|
|
354
|
+
decimated_mesh = self.decimate_mesh(mesh, decimate_factor)
|
|
355
|
+
self.logger.debug("Decimation completed.")
|
|
356
|
+
except Exception as e:
|
|
357
|
+
self.logger.error("Could not decimate background mesh: %s", e)
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
decimated_mesh.export(decimated_save_path)
|
|
361
|
+
self.logger.debug("Decimated background mesh saved: %s", decimated_save_path)
|
|
362
|
+
|
|
363
|
+
self.assets.decimated_background_mesh = decimated_save_path
|
|
364
|
+
|
|
365
|
+
def texture_background_mesh(self) -> None:
|
|
366
|
+
"""Textures the background mesh using satellite imagery."""
|
|
367
|
+
satellite_component = self.map.get_satellite_component()
|
|
368
|
+
if not satellite_component:
|
|
369
|
+
self.logger.warning("Satellite component not found, cannot texture background mesh.")
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
background_texture_path = satellite_component.assets.background
|
|
373
|
+
|
|
374
|
+
if not background_texture_path or not os.path.isfile(background_texture_path):
|
|
375
|
+
self.logger.warning("Background texture not found, cannot texture background mesh.")
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
decimated_background_mesh_path = self.assets.decimated_background_mesh
|
|
379
|
+
if not decimated_background_mesh_path or not os.path.isfile(decimated_background_mesh_path):
|
|
380
|
+
self.logger.warning(
|
|
381
|
+
"Decimated background mesh not found, cannot texture background mesh."
|
|
382
|
+
)
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
background_texture_resolution = self.get_background_texture_resolution(self.map_size)
|
|
386
|
+
non_resized_texture_image = cv2.imread(background_texture_path, cv2.IMREAD_UNCHANGED)
|
|
387
|
+
|
|
388
|
+
if non_resized_texture_image is None:
|
|
389
|
+
self.logger.error(
|
|
390
|
+
"Failed to read background texture image: %s", background_texture_path
|
|
391
|
+
)
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
resized_texture_image = cv2.resize(
|
|
395
|
+
non_resized_texture_image, # type: ignore
|
|
396
|
+
(background_texture_resolution, background_texture_resolution),
|
|
397
|
+
interpolation=cv2.INTER_AREA,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
resized_texture_save_path = os.path.join(
|
|
401
|
+
self.textured_mesh_directory,
|
|
402
|
+
"background_texture.jpg",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
cv2.imwrite(resized_texture_save_path, resized_texture_image)
|
|
406
|
+
self.logger.debug("Resized background texture saved: %s", resized_texture_save_path)
|
|
407
|
+
|
|
408
|
+
decimated_mesh = trimesh.load_mesh(decimated_background_mesh_path, force="mesh")
|
|
409
|
+
|
|
410
|
+
if decimated_mesh is None:
|
|
411
|
+
self.logger.error("Failed to load decimated mesh after all retry attempts")
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
obj_save_path, mtl_save_path = self.texture_mesh(
|
|
416
|
+
decimated_mesh,
|
|
417
|
+
resized_texture_save_path,
|
|
418
|
+
output_directory=self.textured_mesh_directory,
|
|
419
|
+
output_name="background_textured_mesh",
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
self.assets.textured_background_mesh = obj_save_path
|
|
423
|
+
self.assets.textured_background_mtl = mtl_save_path
|
|
424
|
+
self.assets.resized_background_texture = resized_texture_save_path
|
|
425
|
+
self.logger.debug("Textured background mesh saved: %s", obj_save_path)
|
|
426
|
+
except Exception as e:
|
|
427
|
+
self.logger.error("Could not texture background mesh: %s", e)
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
def convert_background_mesh_to_i3d(self) -> bool:
|
|
431
|
+
"""Converts the textured background mesh to i3d format.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
bool -- True if the conversion was successful, False otherwise.
|
|
435
|
+
"""
|
|
436
|
+
if not self.assets.textured_background_mesh or not os.path.isfile(
|
|
437
|
+
self.assets.textured_background_mesh
|
|
438
|
+
):
|
|
439
|
+
self.logger.warning("Textured background mesh not found, cannot convert to i3d.")
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
if not self.assets.resized_background_texture or not os.path.isfile(
|
|
443
|
+
self.assets.resized_background_texture
|
|
444
|
+
):
|
|
445
|
+
self.logger.warning("Resized background texture not found, cannot convert to i3d.")
|
|
446
|
+
return False
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
mesh = trimesh.load_mesh(self.assets.textured_background_mesh, force="mesh")
|
|
450
|
+
except Exception as e:
|
|
451
|
+
self.logger.error("Could not load textured background mesh: %s", e)
|
|
452
|
+
return False
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
i3d_background_terrain = self.mesh_to_i3d(
|
|
456
|
+
mesh,
|
|
457
|
+
output_dir=self.assets_background_directory,
|
|
458
|
+
name=Parameters.BACKGROUND_TERRAIN,
|
|
459
|
+
texture_path=self.assets.resized_background_texture,
|
|
460
|
+
water_mesh=False,
|
|
461
|
+
)
|
|
462
|
+
self.logger.debug(
|
|
463
|
+
"Background mesh converted to i3d successfully: %s", i3d_background_terrain
|
|
464
|
+
)
|
|
465
|
+
self.assets.background_terrain_i3d = i3d_background_terrain
|
|
466
|
+
return True
|
|
467
|
+
except Exception as e:
|
|
468
|
+
self.logger.error("Could not convert background mesh to i3d: %s", e)
|
|
469
|
+
return False
|
|
470
|
+
|
|
471
|
+
def add_note_file(self, asset: Literal["background", "water"]) -> None:
|
|
472
|
+
"""Adds a note file to the background or water directory.
|
|
473
|
+
|
|
474
|
+
Arguments:
|
|
475
|
+
asset (Literal["background", "water"]): The asset type to add the note file to.
|
|
476
|
+
"""
|
|
477
|
+
filename = "DO_NOT_USE_THESE_FILES.txt"
|
|
478
|
+
note_template = (
|
|
479
|
+
"Please find the ready-to-use {asset} i3d files in the {asset_directory} directory."
|
|
480
|
+
)
|
|
481
|
+
directory = {
|
|
482
|
+
"background": self.background_directory,
|
|
483
|
+
"water": self.water_directory,
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
content = (
|
|
487
|
+
"The files in this directory can be used to create the mesh files manually in Blender. "
|
|
488
|
+
"However, it's recommended to use the ready-to-use i3d files located in the assets "
|
|
489
|
+
"directory. There you'll find the i3d files, that can be imported directly into the "
|
|
490
|
+
"Giants Editor without any additional processing."
|
|
491
|
+
)
|
|
492
|
+
note = note_template.format(
|
|
493
|
+
asset=asset,
|
|
494
|
+
asset_directory=f"assets/{asset}",
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
file_path = os.path.join(directory[asset], filename)
|
|
498
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
499
|
+
f.write(content + "\n\n" + note)
|
|
500
|
+
|
|
501
|
+
def convert_water_mesh_to_i3d(self) -> bool:
|
|
502
|
+
"""Converts the line-based water mesh to i3d format.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
bool -- True if the conversion was successful, False otherwise.
|
|
506
|
+
"""
|
|
507
|
+
if not self.assets.line_based_water_mesh or not os.path.isfile(
|
|
508
|
+
self.assets.line_based_water_mesh
|
|
509
|
+
):
|
|
510
|
+
self.logger.warning("Line-based water mesh not found, cannot convert to i3d.")
|
|
511
|
+
return False
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
mesh = trimesh.load_mesh(self.assets.line_based_water_mesh, force="mesh")
|
|
515
|
+
except Exception as e:
|
|
516
|
+
self.logger.error("Could not load line-based water mesh: %s", e)
|
|
517
|
+
return False
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
i3d_water_resources = self.mesh_to_i3d(
|
|
521
|
+
mesh,
|
|
522
|
+
output_dir=self.assets_water_directory,
|
|
523
|
+
name=Parameters.WATER_RESOURCES,
|
|
524
|
+
water_mesh=True,
|
|
525
|
+
)
|
|
526
|
+
self.logger.debug(
|
|
527
|
+
"Water resources mesh converted to i3d successfully: %s", i3d_water_resources
|
|
528
|
+
)
|
|
529
|
+
self.assets.water_resources_i3d = i3d_water_resources
|
|
530
|
+
return True
|
|
531
|
+
except Exception as e:
|
|
532
|
+
self.logger.error("Could not convert water mesh to i3d: %s", e)
|
|
533
|
+
return False
|
|
534
|
+
|
|
256
535
|
def save_map_dem(self, dem_path: str, save_path: str | None = None) -> str:
|
|
257
536
|
"""Cuts out the center of the DEM (the actual map) and saves it as a separate file.
|
|
258
537
|
|
|
@@ -623,6 +902,8 @@ class Background(MeshComponent, ImageComponent):
|
|
|
623
902
|
mesh.export(line_based_save_path)
|
|
624
903
|
self.logger.debug("Line-based water mesh saved to %s", line_based_save_path)
|
|
625
904
|
|
|
905
|
+
self.assets.line_based_water_mesh = line_based_save_path
|
|
906
|
+
|
|
626
907
|
def mesh_from_3d_polygons(
|
|
627
908
|
self, polygons: list[shapely.Polygon], single_z_value: int | None = None
|
|
628
909
|
) -> Trimesh | None:
|
|
@@ -22,12 +22,31 @@ if TYPE_CHECKING:
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class AttrDict(dict):
|
|
25
|
-
"""A dictionary that allows attribute-style access to its keys.
|
|
25
|
+
"""A dictionary that allows attribute-style access to its keys.
|
|
26
|
+
Allows safe access to non-existing keys, returning None instead of raising KeyError.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __getattr__(self, name: str) -> Any | None:
|
|
30
|
+
"""Returns the value of the given key or None if the key does not exist.
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
Arguments:
|
|
33
|
+
name (str): The key to retrieve.
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
Returns:
|
|
36
|
+
Any | None: The value of the key or None if the key does not exist.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
return self[name]
|
|
40
|
+
except KeyError:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
44
|
+
"""Sets the value of the given key.
|
|
45
|
+
|
|
46
|
+
Arguments:
|
|
47
|
+
name (str): The key to set.
|
|
48
|
+
value (Any): The value to set.
|
|
49
|
+
"""
|
|
31
50
|
self[name] = value
|
|
32
51
|
|
|
33
52
|
|
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
|
|
5
5
|
import cv2
|
|
6
6
|
import numpy as np
|
|
7
|
+
from PIL import Image, ImageFile
|
|
7
8
|
|
|
8
9
|
from maps4fs.generator.component.base.component import Component
|
|
9
10
|
from maps4fs.generator.settings import Parameters
|
|
@@ -237,3 +238,35 @@ class ImageComponent(Component):
|
|
|
237
238
|
edge_mask = cv2.subtract(bigger_mask, smaller_mask)
|
|
238
239
|
|
|
239
240
|
return self.blur_by_mask(data, edge_mask)
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def convert_png_to_dds(input_png_path: str, output_dds_path: str):
|
|
244
|
+
"""Convert a PNG file to DDS format using PIL
|
|
245
|
+
|
|
246
|
+
Arguments:
|
|
247
|
+
input_png_path (str): Path to input PNG file
|
|
248
|
+
output_dds_path (str): Path for output DDS file
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
FileNotFoundError: If the input PNG file does not exist.
|
|
252
|
+
RuntimeError: If the DDS conversion fails.
|
|
253
|
+
"""
|
|
254
|
+
if not os.path.exists(input_png_path):
|
|
255
|
+
raise FileNotFoundError(f"Input PNG file not found: {input_png_path}")
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
259
|
+
|
|
260
|
+
with Image.open(input_png_path) as img:
|
|
261
|
+
# Convert to RGB if needed (DDS works better with RGB)
|
|
262
|
+
if img.mode == "RGBA":
|
|
263
|
+
# Create RGB version on white background
|
|
264
|
+
rgb_img = Image.new("RGB", img.size, (255, 255, 255))
|
|
265
|
+
rgb_img.paste(img, mask=img.split()[-1]) # Use alpha as mask
|
|
266
|
+
img = rgb_img
|
|
267
|
+
elif img.mode != "RGB":
|
|
268
|
+
img = img.convert("RGB")
|
|
269
|
+
|
|
270
|
+
img.save(output_dds_path, format="DDS")
|
|
271
|
+
except Exception as e:
|
|
272
|
+
raise RuntimeError(f"DDS conversion failed: {e}")
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"""Base class for all components that primarily used to work with meshes."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
from datetime import datetime
|
|
4
7
|
|
|
5
8
|
import cv2
|
|
6
9
|
import numpy as np
|
|
10
|
+
import open3d as o3d
|
|
7
11
|
import trimesh
|
|
12
|
+
from PIL import Image
|
|
8
13
|
from tqdm import tqdm
|
|
9
14
|
|
|
10
15
|
from maps4fs.generator.component.base.component import Component
|
|
@@ -269,3 +274,405 @@ class MeshComponent(Component):
|
|
|
269
274
|
mesh_copy = mesh.copy()
|
|
270
275
|
mesh_copy.faces = mesh_copy.faces[:, ::-1] # type: ignore
|
|
271
276
|
return mesh_copy
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def decimate_mesh(mesh: trimesh.Trimesh, reduction_factor: float) -> trimesh.Trimesh:
|
|
280
|
+
"""Decimate mesh using Open3D's quadric decimation (similar to Blender's approach)
|
|
281
|
+
|
|
282
|
+
Arguments:
|
|
283
|
+
mesh (trimesh.Trimesh): Input trimesh mesh
|
|
284
|
+
reduction_factor (float): Reduce to this fraction of original triangles (0.5 = 50%)
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
trimesh.Trimesh: Decimated trimesh mesh
|
|
288
|
+
"""
|
|
289
|
+
# 1. Convert trimesh to Open3D format.
|
|
290
|
+
vertices = mesh.vertices
|
|
291
|
+
faces = mesh.faces
|
|
292
|
+
|
|
293
|
+
o3d_mesh = o3d.geometry.TriangleMesh()
|
|
294
|
+
o3d_mesh.vertices = o3d.utility.Vector3dVector(vertices)
|
|
295
|
+
o3d_mesh.triangles = o3d.utility.Vector3iVector(faces)
|
|
296
|
+
|
|
297
|
+
# 2. Calculate target number of triangles.
|
|
298
|
+
target_triangles = int(len(faces) * reduction_factor)
|
|
299
|
+
|
|
300
|
+
# 3. Apply quadric decimation.
|
|
301
|
+
decimated_o3d = o3d_mesh.simplify_quadric_decimation(target_triangles)
|
|
302
|
+
|
|
303
|
+
# 4. Convert back to trimesh.
|
|
304
|
+
decimated_vertices = np.asarray(decimated_o3d.vertices)
|
|
305
|
+
decimated_faces = np.asarray(decimated_o3d.triangles)
|
|
306
|
+
decimated_mesh = trimesh.Trimesh(vertices=decimated_vertices, faces=decimated_faces)
|
|
307
|
+
|
|
308
|
+
return decimated_mesh
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def texture_mesh(
|
|
312
|
+
mesh: trimesh.Trimesh,
|
|
313
|
+
resized_texture_path: str,
|
|
314
|
+
output_directory: str,
|
|
315
|
+
output_name: str,
|
|
316
|
+
) -> tuple[str, str]:
|
|
317
|
+
"""Apply texture to mesh with UV mapping based on X and Z coordinates (ground plane).
|
|
318
|
+
|
|
319
|
+
Arguments:
|
|
320
|
+
mesh (trimesh.Trimesh): The mesh to texture
|
|
321
|
+
resized_texture_path (str): Path to resized texture image
|
|
322
|
+
output_directory (str): Directory to save textured OBJ and MTL files and texture.
|
|
323
|
+
output_name (str): Base name for output files (without extension)
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
tuple[str, str]: Paths to the saved OBJ and MTL files
|
|
327
|
+
"""
|
|
328
|
+
# 1. Copy texture to output directory (only if not already there).
|
|
329
|
+
texture_filename = os.path.basename(resized_texture_path)
|
|
330
|
+
texture_output_path = os.path.join(output_directory, texture_filename)
|
|
331
|
+
|
|
332
|
+
# Check if source and destination are the same file to avoid copy conflicts
|
|
333
|
+
if os.path.abspath(resized_texture_path) != os.path.abspath(texture_output_path):
|
|
334
|
+
shutil.copy2(resized_texture_path, texture_output_path)
|
|
335
|
+
|
|
336
|
+
# 2. Apply rotation to fix 90-degree X-axis rotation.
|
|
337
|
+
rotation_matrix = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0])
|
|
338
|
+
mesh.apply_transform(rotation_matrix)
|
|
339
|
+
|
|
340
|
+
# 3. Get mesh bounds: using X and Z for ground plane.
|
|
341
|
+
vertices = mesh.vertices
|
|
342
|
+
|
|
343
|
+
# 4. Ground plane coordinates (X, Z)
|
|
344
|
+
min_x = np.min(vertices[:, 0]) # X coordinate
|
|
345
|
+
max_x = np.max(vertices[:, 0])
|
|
346
|
+
min_z = np.min(vertices[:, 2]) # Z coordinate
|
|
347
|
+
max_z = np.max(vertices[:, 2])
|
|
348
|
+
|
|
349
|
+
width = max_x - min_x # X dimension
|
|
350
|
+
depth = max_z - min_z # Z dimension
|
|
351
|
+
|
|
352
|
+
# 5. Load texture.
|
|
353
|
+
texture_image = Image.open(texture_output_path)
|
|
354
|
+
|
|
355
|
+
# 6. Calculate UV coordinates based on X and Z positions.
|
|
356
|
+
uv_coords = np.zeros((len(vertices), 2), dtype=np.float32)
|
|
357
|
+
for i in tqdm(range(len(vertices)), desc="Calculating UVs", unit="vertex"):
|
|
358
|
+
vertex = vertices[i]
|
|
359
|
+
|
|
360
|
+
# Map X coordinate to U
|
|
361
|
+
u = (vertex[0] - min_x) / width if width > 0 else 0.5
|
|
362
|
+
|
|
363
|
+
# Map Z coordinate to V (NOT Y!)
|
|
364
|
+
v = (vertex[2] - min_z) / depth if depth > 0 else 0.5
|
|
365
|
+
|
|
366
|
+
# Flip V coordinate for correct orientation
|
|
367
|
+
v = 1.0 - v
|
|
368
|
+
|
|
369
|
+
# Clamp to valid range
|
|
370
|
+
u = np.clip(u, 0.0, 1.0)
|
|
371
|
+
v = np.clip(v, 0.0, 1.0)
|
|
372
|
+
|
|
373
|
+
uv_coords[i] = [u, v]
|
|
374
|
+
|
|
375
|
+
# 7. Create material.
|
|
376
|
+
material = trimesh.visual.material.PBRMaterial(
|
|
377
|
+
baseColorTexture=texture_image,
|
|
378
|
+
metallicFactor=0.0,
|
|
379
|
+
roughnessFactor=1.0,
|
|
380
|
+
emissiveFactor=[0.0, 0.0, 0.0],
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# 8. Apply UV and material to mesh.
|
|
384
|
+
visual = trimesh.visual.TextureVisuals(uv=uv_coords, material=material)
|
|
385
|
+
mesh.visual = visual
|
|
386
|
+
|
|
387
|
+
mtl_filename = f"{output_name}.mtl"
|
|
388
|
+
obj_filename = f"{output_name}.obj"
|
|
389
|
+
mtl_filepath = os.path.join(output_directory, mtl_filename)
|
|
390
|
+
obj_filepath = os.path.join(output_directory, obj_filename)
|
|
391
|
+
|
|
392
|
+
faces = mesh.faces
|
|
393
|
+
|
|
394
|
+
# 9. Write OBJ file with correct UV mapping.
|
|
395
|
+
with open(obj_filepath, "w") as f: # pylint: disable=unspecified-encoding
|
|
396
|
+
f.write("# Corrected UV mapping using X,Z coordinates (ground plane)\n")
|
|
397
|
+
f.write("# Y coordinate represents elevation\n")
|
|
398
|
+
f.write(f"mtllib {os.path.basename(mtl_filename)}\n")
|
|
399
|
+
|
|
400
|
+
# Write vertices
|
|
401
|
+
for vertex in vertices:
|
|
402
|
+
f.write(f"v {vertex[0]:.6f} {vertex[1]:.6f} {vertex[2]:.6f}\n")
|
|
403
|
+
|
|
404
|
+
# Write UV coordinates
|
|
405
|
+
for uv in uv_coords:
|
|
406
|
+
f.write(f"vt {uv[0]:.6f} {uv[1]:.6f}\n")
|
|
407
|
+
|
|
408
|
+
# Write faces
|
|
409
|
+
f.write("usemtl TerrainMaterial_XZ\n")
|
|
410
|
+
for face in faces:
|
|
411
|
+
v1, v2, v3 = face[0] + 1, face[1] + 1, face[2] + 1
|
|
412
|
+
f.write(f"f {v1}/{v1} {v2}/{v2} {v3}/{v3}\n")
|
|
413
|
+
|
|
414
|
+
# 10. Write MTL file.
|
|
415
|
+
with open(mtl_filepath, "w") as f: # pylint: disable=unspecified-encoding
|
|
416
|
+
f.write("# Material with X,Z UV mapping\n")
|
|
417
|
+
f.write("newmtl TerrainMaterial_XZ\n")
|
|
418
|
+
f.write("Ka 1.0 1.0 1.0\n")
|
|
419
|
+
f.write("Kd 1.0 1.0 1.0\n")
|
|
420
|
+
f.write("Ks 0.0 0.0 0.0\n")
|
|
421
|
+
f.write("illum 1\n")
|
|
422
|
+
f.write(f"map_Kd {texture_filename}\n")
|
|
423
|
+
|
|
424
|
+
return obj_filepath, mtl_filepath
|
|
425
|
+
|
|
426
|
+
@staticmethod
|
|
427
|
+
def mesh_to_i3d(
|
|
428
|
+
mesh: trimesh.Trimesh,
|
|
429
|
+
output_dir: str,
|
|
430
|
+
name: str,
|
|
431
|
+
texture_path: str | None = None,
|
|
432
|
+
water_mesh: bool = False,
|
|
433
|
+
) -> str:
|
|
434
|
+
"""Convert a trimesh to i3d format with optional water shader support.
|
|
435
|
+
|
|
436
|
+
Arguments:
|
|
437
|
+
mesh (trimesh.Trimesh): trimesh.Trimesh object to convert
|
|
438
|
+
output_dir (str): Directory to save i3d and copy textures to
|
|
439
|
+
name (str): Base name for output files (e.g., "terrain_mesh")
|
|
440
|
+
texture_path (str | None): Optional path to texture file (will be copied to output_dir)
|
|
441
|
+
water_mesh (bool): If True, adds ocean shader material for water rendering
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
str: Full path to the generated i3d file
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
# Ensure output directory exists
|
|
448
|
+
if not os.path.exists(output_dir):
|
|
449
|
+
os.makedirs(output_dir)
|
|
450
|
+
|
|
451
|
+
# Apply transformations (only for water meshes)
|
|
452
|
+
if water_mesh:
|
|
453
|
+
# 1. Apply rotation fix (90-degree X-axis correction) - water only
|
|
454
|
+
rotation_matrix = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0])
|
|
455
|
+
mesh.apply_transform(rotation_matrix)
|
|
456
|
+
|
|
457
|
+
# 2. Center mesh at origin - water only
|
|
458
|
+
vertices = mesh.vertices
|
|
459
|
+
center = vertices.mean(axis=0)
|
|
460
|
+
mesh.vertices = vertices - center
|
|
461
|
+
|
|
462
|
+
# 3. Handle texture copying if provided
|
|
463
|
+
texture_file = None
|
|
464
|
+
if texture_path and os.path.exists(texture_path):
|
|
465
|
+
texture_filename = os.path.basename(texture_path)
|
|
466
|
+
texture_dest = os.path.join(output_dir, texture_filename)
|
|
467
|
+
|
|
468
|
+
# Copy texture if it's not already in output_dir
|
|
469
|
+
if os.path.abspath(texture_path) != os.path.abspath(texture_dest):
|
|
470
|
+
shutil.copy2(texture_path, texture_dest)
|
|
471
|
+
|
|
472
|
+
texture_file = texture_filename
|
|
473
|
+
|
|
474
|
+
# 4. Generate i3d file
|
|
475
|
+
output_path = os.path.join(output_dir, f"{name}.i3d")
|
|
476
|
+
MeshComponent._write_i3d_file(mesh, output_path, name, texture_file, water_mesh)
|
|
477
|
+
|
|
478
|
+
return output_path
|
|
479
|
+
|
|
480
|
+
@staticmethod
|
|
481
|
+
def _write_i3d_file(
|
|
482
|
+
mesh: trimesh.Trimesh,
|
|
483
|
+
output_path: str,
|
|
484
|
+
name: str,
|
|
485
|
+
texture_file: str | None,
|
|
486
|
+
is_water: bool,
|
|
487
|
+
) -> None:
|
|
488
|
+
"""Write the actual i3d XML file.
|
|
489
|
+
|
|
490
|
+
Arguments:
|
|
491
|
+
mesh (trimesh.Trimesh): object containing the geometry
|
|
492
|
+
output_path (str): Full path where to save the i3d file
|
|
493
|
+
name (str): Name for the mesh in i3d file
|
|
494
|
+
texture_file (str | None): Optional texture filename (if copied to output dir)
|
|
495
|
+
is_water (bool): If True, generates water mesh with ocean shader
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
# Root element
|
|
499
|
+
i3d = ET.Element(
|
|
500
|
+
"i3D",
|
|
501
|
+
attrib={
|
|
502
|
+
"name": name,
|
|
503
|
+
"version": "1.6",
|
|
504
|
+
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
505
|
+
"xsi:noNamespaceSchemaLocation": "http://i3d.giants.ch/schema/i3d-1.6.xsd",
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Asset section
|
|
510
|
+
asset = ET.SubElement(i3d, "Asset")
|
|
511
|
+
exp = ET.SubElement(asset, "Export")
|
|
512
|
+
exp.set("program", "maps4fs")
|
|
513
|
+
exp.set("version", "1.0")
|
|
514
|
+
exp.set("date", datetime.now().strftime("%Y-%m-%d"))
|
|
515
|
+
|
|
516
|
+
vertices = mesh.vertices
|
|
517
|
+
faces = mesh.faces
|
|
518
|
+
|
|
519
|
+
has_normals = mesh.vertex_normals is not None and len(mesh.vertex_normals) == len(vertices)
|
|
520
|
+
has_uv = (
|
|
521
|
+
hasattr(mesh.visual, "uv")
|
|
522
|
+
and mesh.visual.uv is not None
|
|
523
|
+
and len(mesh.visual.uv) == len(vertices)
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Files section
|
|
527
|
+
files_section = None
|
|
528
|
+
if is_water:
|
|
529
|
+
# Water mesh: add ocean shader
|
|
530
|
+
files_section = ET.SubElement(i3d, "Files")
|
|
531
|
+
shader_file = ET.SubElement(files_section, "File")
|
|
532
|
+
shader_file.set("fileId", "4")
|
|
533
|
+
shader_file.set("filename", "$data/shaders/oceanShader.xml")
|
|
534
|
+
elif texture_file:
|
|
535
|
+
# Terrain mesh: add texture file
|
|
536
|
+
files_section = ET.SubElement(i3d, "Files")
|
|
537
|
+
file_entry = ET.SubElement(files_section, "File")
|
|
538
|
+
file_entry.set("fileId", "1")
|
|
539
|
+
file_entry.set("filename", texture_file)
|
|
540
|
+
file_entry.set("relativePath", "true")
|
|
541
|
+
|
|
542
|
+
# Materials section
|
|
543
|
+
materials_section = ET.SubElement(i3d, "Materials")
|
|
544
|
+
material = ET.SubElement(materials_section, "Material")
|
|
545
|
+
|
|
546
|
+
if is_water:
|
|
547
|
+
# Water material with ocean shader
|
|
548
|
+
material.set("name", "OceanShader")
|
|
549
|
+
material.set("materialId", "1")
|
|
550
|
+
material.set("diffuseColor", "0.8 0.8 0.8 1")
|
|
551
|
+
material.set("specularColor", "0.501961 1 0")
|
|
552
|
+
material.set("customShaderId", "4")
|
|
553
|
+
material.set("customShaderVariation", "simple")
|
|
554
|
+
|
|
555
|
+
# Required for ocean shader
|
|
556
|
+
normalmap = ET.SubElement(material, "Normalmap")
|
|
557
|
+
normalmap.set("fileId", "2")
|
|
558
|
+
|
|
559
|
+
refractionmap = ET.SubElement(material, "Refractionmap")
|
|
560
|
+
refractionmap.set("coeff", "1")
|
|
561
|
+
refractionmap.set("bumpScale", "0.01")
|
|
562
|
+
refractionmap.set("withSSRData", "true")
|
|
563
|
+
else:
|
|
564
|
+
# Standard terrain material
|
|
565
|
+
material.set("name", f"{name}_material")
|
|
566
|
+
material.set("materialId", "1")
|
|
567
|
+
material.set("diffuseColor", "1 1 1 1")
|
|
568
|
+
material.set("specularColor", "0.5 0.5 0.5")
|
|
569
|
+
|
|
570
|
+
if texture_file:
|
|
571
|
+
texture = ET.SubElement(material, "Texture")
|
|
572
|
+
texture.set("fileId", "1")
|
|
573
|
+
|
|
574
|
+
# Shapes section
|
|
575
|
+
shapes = ET.SubElement(i3d, "Shapes")
|
|
576
|
+
shape = ET.SubElement(shapes, "IndexedTriangleSet")
|
|
577
|
+
shape.set("name", name)
|
|
578
|
+
shape.set("shapeId", "1")
|
|
579
|
+
|
|
580
|
+
# Calculate bounding sphere
|
|
581
|
+
if len(vertices) > 0:
|
|
582
|
+
center = vertices.mean(axis=0)
|
|
583
|
+
max_dist = ((vertices - center) ** 2).sum(axis=1).max() ** 0.5
|
|
584
|
+
shape.set("bvCenter", f"{center[0]:.6f} {center[1]:.6f} {center[2]:.6f}")
|
|
585
|
+
shape.set("bvRadius", f"{max_dist:.6f}")
|
|
586
|
+
|
|
587
|
+
# Vertices block
|
|
588
|
+
xml_vertices = ET.SubElement(shape, "Vertices")
|
|
589
|
+
xml_vertices.set("count", str(len(vertices)))
|
|
590
|
+
|
|
591
|
+
if has_normals:
|
|
592
|
+
xml_vertices.set("normal", "true")
|
|
593
|
+
if has_uv:
|
|
594
|
+
xml_vertices.set("uv0", "true")
|
|
595
|
+
|
|
596
|
+
# Write vertex data
|
|
597
|
+
for idx in tqdm(range(len(vertices)), desc="Writing vertices", unit="vertex"):
|
|
598
|
+
v = vertices[idx]
|
|
599
|
+
v_el = ET.SubElement(xml_vertices, "v")
|
|
600
|
+
v_el.set("p", f"{v[0]:.6f} {v[1]:.6f} {v[2]:.6f}")
|
|
601
|
+
|
|
602
|
+
if has_normals:
|
|
603
|
+
n = mesh.vertex_normals[idx]
|
|
604
|
+
v_el.set("n", f"{n[0]:.6f} {n[1]:.6f} {n[2]:.6f}")
|
|
605
|
+
|
|
606
|
+
if has_uv:
|
|
607
|
+
uv = mesh.visual.uv[idx]
|
|
608
|
+
v_el.set("t0", f"{uv[0]:.6f} {uv[1]:.6f}")
|
|
609
|
+
|
|
610
|
+
# Triangles block
|
|
611
|
+
xml_tris = ET.SubElement(shape, "Triangles")
|
|
612
|
+
xml_tris.set("count", str(len(faces)))
|
|
613
|
+
for f in tqdm(faces, desc="Writing triangles", unit="triangle"):
|
|
614
|
+
t = ET.SubElement(xml_tris, "t")
|
|
615
|
+
t.set("vi", f"{f[0]} {f[1]} {f[2]}")
|
|
616
|
+
|
|
617
|
+
# Subsets block
|
|
618
|
+
xml_subs = ET.SubElement(shape, "Subsets")
|
|
619
|
+
xml_subs.set("count", "1")
|
|
620
|
+
subset = ET.SubElement(xml_subs, "Subset")
|
|
621
|
+
subset.set("firstVertex", "0")
|
|
622
|
+
subset.set("numVertices", str(len(vertices)))
|
|
623
|
+
subset.set("firstIndex", "0")
|
|
624
|
+
subset.set("numIndices", str(len(faces) * 3))
|
|
625
|
+
|
|
626
|
+
# Scene section
|
|
627
|
+
scene = ET.SubElement(i3d, "Scene")
|
|
628
|
+
|
|
629
|
+
if is_water:
|
|
630
|
+
# Water: direct shape node
|
|
631
|
+
shape_node = ET.SubElement(scene, "Shape")
|
|
632
|
+
shape_node.set("name", name)
|
|
633
|
+
shape_node.set("shapeId", "1")
|
|
634
|
+
shape_node.set("nodeId", "4")
|
|
635
|
+
shape_node.set("castsShadows", "true")
|
|
636
|
+
shape_node.set("receiveShadows", "true")
|
|
637
|
+
shape_node.set("materialIds", "1")
|
|
638
|
+
else:
|
|
639
|
+
# Terrain: transform group with shape
|
|
640
|
+
transform_group = ET.SubElement(scene, "TransformGroup")
|
|
641
|
+
transform_group.set("name", name)
|
|
642
|
+
transform_group.set("nodeId", "1")
|
|
643
|
+
|
|
644
|
+
shape_node = ET.SubElement(transform_group, "Shape")
|
|
645
|
+
shape_node.set("name", f"{name}_shape")
|
|
646
|
+
shape_node.set("nodeId", "2")
|
|
647
|
+
shape_node.set("shapeId", "1")
|
|
648
|
+
shape_node.set("static", "true")
|
|
649
|
+
shape_node.set("compound", "false")
|
|
650
|
+
shape_node.set("collision", "true")
|
|
651
|
+
shape_node.set("materialIds", "1")
|
|
652
|
+
|
|
653
|
+
# Pretty print and write
|
|
654
|
+
MeshComponent._indent(i3d)
|
|
655
|
+
tree = ET.ElementTree(i3d)
|
|
656
|
+
tree.write(output_path, encoding="iso-8859-1", xml_declaration=True)
|
|
657
|
+
|
|
658
|
+
@staticmethod
|
|
659
|
+
def _indent(elem: ET.Element, level: int = 0) -> None:
|
|
660
|
+
"""Pretty print XML formatting. Modifies the element in place.
|
|
661
|
+
|
|
662
|
+
Arguments:
|
|
663
|
+
elem (ET.Element): The XML element to indent
|
|
664
|
+
level (int): Current indentation level
|
|
665
|
+
"""
|
|
666
|
+
i = "\n" + level * " "
|
|
667
|
+
if len(elem):
|
|
668
|
+
if not elem.text or not elem.text.strip():
|
|
669
|
+
elem.text = i + " "
|
|
670
|
+
if not elem.tail or not elem.tail.strip():
|
|
671
|
+
elem.tail = i
|
|
672
|
+
for e in elem:
|
|
673
|
+
MeshComponent._indent(e, level + 1)
|
|
674
|
+
if not elem.tail or not elem.tail.strip():
|
|
675
|
+
elem.tail = i
|
|
676
|
+
else:
|
|
677
|
+
if level and (not elem.tail or not elem.tail.strip()):
|
|
678
|
+
elem.tail = i
|
maps4fs/generator/game.py
CHANGED
|
@@ -42,9 +42,10 @@ class Game:
|
|
|
42
42
|
_environment_processing: bool = True
|
|
43
43
|
_fog_processing: bool = True
|
|
44
44
|
_dissolve: bool = True
|
|
45
|
+
_mesh_processing: bool = True
|
|
45
46
|
|
|
46
47
|
# Order matters! Some components depend on others.
|
|
47
|
-
components = [Texture, Background, GRLE, I3d, Config
|
|
48
|
+
components = [Satellite, Texture, Background, GRLE, I3d, Config]
|
|
48
49
|
|
|
49
50
|
def __init__(self, map_template_path: str | None = None):
|
|
50
51
|
if map_template_path:
|
|
@@ -312,6 +313,14 @@ class Game:
|
|
|
312
313
|
bool: True if the dissolve should be applied, False otherwise."""
|
|
313
314
|
return self._dissolve
|
|
314
315
|
|
|
316
|
+
@property
|
|
317
|
+
def mesh_processing(self) -> bool:
|
|
318
|
+
"""Returns whether the mesh should be processed.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
bool: True if the mesh should be processed, False otherwise."""
|
|
322
|
+
return self._mesh_processing
|
|
323
|
+
|
|
315
324
|
|
|
316
325
|
class FS22(Game):
|
|
317
326
|
"""Class used to define the game version FS22."""
|
|
@@ -324,6 +333,7 @@ class FS22(Game):
|
|
|
324
333
|
_fog_processing = False
|
|
325
334
|
_plants_processing = False
|
|
326
335
|
_dissolve = False
|
|
336
|
+
_mesh_processing = False
|
|
327
337
|
|
|
328
338
|
def dem_file_path(self, map_directory: str) -> str:
|
|
329
339
|
"""Returns the path to the DEM file.
|
maps4fs/generator/map.py
CHANGED
|
@@ -12,7 +12,7 @@ from pydtmdl.base.dtm import DTMProviderSettings
|
|
|
12
12
|
|
|
13
13
|
import maps4fs.generator.config as mfscfg
|
|
14
14
|
import maps4fs.generator.utils as mfsutils
|
|
15
|
-
from maps4fs.generator.component import Background, Component, Layer, Texture
|
|
15
|
+
from maps4fs.generator.component import Background, Component, Layer, Satellite, Texture
|
|
16
16
|
from maps4fs.generator.game import Game
|
|
17
17
|
from maps4fs.generator.settings import GenerationSettings, MainSettings, SharedSettings
|
|
18
18
|
from maps4fs.generator.statistics import send_advanced_settings, send_main_settings
|
|
@@ -150,6 +150,9 @@ class Map:
|
|
|
150
150
|
raise RuntimeError(f"Can not unpack map template due to error: {e}") from e
|
|
151
151
|
# endregion
|
|
152
152
|
|
|
153
|
+
self.assets_directory = os.path.join(self.map_directory, "assets")
|
|
154
|
+
os.makedirs(self.assets_directory, exist_ok=True)
|
|
155
|
+
|
|
153
156
|
self.shared_settings = SharedSettings()
|
|
154
157
|
self.components: list[Component] = []
|
|
155
158
|
|
|
@@ -301,6 +304,17 @@ class Map:
|
|
|
301
304
|
return None
|
|
302
305
|
return component
|
|
303
306
|
|
|
307
|
+
def get_satellite_component(self) -> Satellite | None:
|
|
308
|
+
"""Get satellite component.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Satellite | None: Satellite instance or None if not found.
|
|
312
|
+
"""
|
|
313
|
+
component = self.get_component("Satellite")
|
|
314
|
+
if not isinstance(component, Satellite):
|
|
315
|
+
return None
|
|
316
|
+
return component
|
|
317
|
+
|
|
304
318
|
def get_texture_layer(self, by_usage: str | None = None) -> Layer | None:
|
|
305
319
|
"""Get texture layer by usage.
|
|
306
320
|
|
maps4fs/generator/settings.py
CHANGED
|
@@ -28,12 +28,19 @@ class Parameters:
|
|
|
28
28
|
WATER = "water"
|
|
29
29
|
FARMYARDS = "farmyards"
|
|
30
30
|
|
|
31
|
+
MAXIMUM_BACKGROUND_TEXTURE_SIZE = 4096
|
|
32
|
+
|
|
31
33
|
PREVIEW_MAXIMUM_SIZE = 2048
|
|
32
34
|
|
|
33
35
|
BACKGROUND_DISTANCE = 2048
|
|
34
36
|
FULL = "FULL"
|
|
35
37
|
PREVIEW = "PREVIEW"
|
|
36
38
|
|
|
39
|
+
DECIMATED_BACKGROUND = "decimated_background"
|
|
40
|
+
BACKGROUND_TERRAIN = "background_terrain"
|
|
41
|
+
|
|
42
|
+
WATER_RESOURCES = "water_resources"
|
|
43
|
+
|
|
37
44
|
RESIZE_FACTOR = 8
|
|
38
45
|
|
|
39
46
|
FARMLAND_ID_LIMIT = 254
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maps4fs
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.3
|
|
4
4
|
Summary: Generate map templates for Farming Simulator from real places.
|
|
5
5
|
Author-email: iwatkot <iwatkot@gmail.com>
|
|
6
6
|
License: GNU Affero General Public License v3.0
|
|
@@ -26,18 +26,22 @@ Requires-Dist: scipy
|
|
|
26
26
|
Requires-Dist: pydtmdl
|
|
27
27
|
Requires-Dist: manifold3d
|
|
28
28
|
Requires-Dist: fast-simplification
|
|
29
|
+
Requires-Dist: open3d
|
|
29
30
|
Dynamic: license-file
|
|
30
31
|
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
<div align="center" markdown>
|
|
33
|
+
|
|
34
|
+
[](https://github.com/iwatkot/maps4fs)
|
|
35
|
+
[](https://github.com/iwatkot/pydtmdl)
|
|
36
|
+
[](https://github.com/iwatkot/pygmdl)
|
|
37
|
+
[](https://github.com/iwatkot/maps4fsapi)
|
|
38
|
+
[](https://github.com/iwatkot/maps4fsui)
|
|
39
|
+
[](https://github.com/iwatkot/maps4fsdata)
|
|
40
|
+
[](https://github.com/iwatkot/maps4fsupgrader)
|
|
41
|
+
[](https://github.com/iwatkot/maps4fsstats)
|
|
42
|
+
[](https://github.com/iwatkot/maps4fsbot)
|
|
43
|
+
|
|
44
|
+
</div>
|
|
41
45
|
|
|
42
46
|
<div align="center" markdown>
|
|
43
47
|
<a href="https://discord.gg/Sj5QKKyE42">
|
|
@@ -2,14 +2,14 @@ maps4fs/__init__.py,sha256=5ixsCA5vgcIV0OrF9EJBm91Mmc_KfMiDRM-QyifMAvo,386
|
|
|
2
2
|
maps4fs/logger.py,sha256=6sem0aFKQqtVjQ_yNu9iGcc-hqzLQUhfxco05K6nqow,763
|
|
3
3
|
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
|
4
4
|
maps4fs/generator/config.py,sha256=KwYYWM5wrMB_tKn3Fls6WpEurZJxmrBH5AgAXPGHTJE,7122
|
|
5
|
-
maps4fs/generator/game.py,sha256=
|
|
6
|
-
maps4fs/generator/map.py,sha256=
|
|
5
|
+
maps4fs/generator/game.py,sha256=sNyPNHGd5bEi7-00l5_MVp62KnBmH-OIIabeDwFEepc,14767
|
|
6
|
+
maps4fs/generator/map.py,sha256=ZZRU8x0feGbgeJgxc0D3N-mfiasyFXxj6gbGyl-WRzE,14528
|
|
7
7
|
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
|
8
|
-
maps4fs/generator/settings.py,sha256=
|
|
8
|
+
maps4fs/generator/settings.py,sha256=4CDyJdKthVAT5I78_1gcruZVK4TYD1ctklAZBwvFwCM,13226
|
|
9
9
|
maps4fs/generator/statistics.py,sha256=Dp1-NS-DWv0l0UdmhOoXeQs_N-Hs7svYUnmziSW5Z9I,2098
|
|
10
10
|
maps4fs/generator/utils.py,sha256=ugdQ8C22NeiZLIlldLoEKCc7ioOefz4W-8qF2eOy9qU,4834
|
|
11
11
|
maps4fs/generator/component/__init__.py,sha256=s01yVVVi8R2xxNvflu2D6wTd9I_g73AMM2x7vAC7GX4,490
|
|
12
|
-
maps4fs/generator/component/background.py,sha256=
|
|
12
|
+
maps4fs/generator/component/background.py,sha256=tFWdYASUUZsJ8QiQrxxba-6aMECD3hvpd0pMm_UdrH8,46151
|
|
13
13
|
maps4fs/generator/component/config.py,sha256=uL76h9UwyhZKZmbxz0mBmWtEPN6qYay4epTEqqtej60,8601
|
|
14
14
|
maps4fs/generator/component/dem.py,sha256=FPqcXmFQg5MPaGuy4g5kxzvY1wbhozeCf-aNMCj5eaU,11687
|
|
15
15
|
maps4fs/generator/component/grle.py,sha256=0PC1K829wjD4y4d9qfIbnU29ebjflIPBbwIZx8FXwc8,27242
|
|
@@ -18,12 +18,12 @@ maps4fs/generator/component/layer.py,sha256=bdy1XGOODyPqYUM3b_wEY2H9Piz-AaHsCDec
|
|
|
18
18
|
maps4fs/generator/component/satellite.py,sha256=9nKwL8zQ-BB6WFMx2m8zduFn6RaxSNv6Vtpge1-QMYE,5052
|
|
19
19
|
maps4fs/generator/component/texture.py,sha256=gXZgr73ehT3_qjuIku0j7N7exloywtmmEQKBJ-MDcco,36988
|
|
20
20
|
maps4fs/generator/component/base/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
|
21
|
-
maps4fs/generator/component/base/component.py,sha256
|
|
22
|
-
maps4fs/generator/component/base/component_image.py,sha256=
|
|
23
|
-
maps4fs/generator/component/base/component_mesh.py,sha256=
|
|
21
|
+
maps4fs/generator/component/base/component.py,sha256=-7H3donrH19f0_rivNyI3fgLsiZkntXfGywEx4tOnM4,23924
|
|
22
|
+
maps4fs/generator/component/base/component_image.py,sha256=GXFkEFARNRkWkDiGSjvU4WX6f_8s6R1t2ZYqZflv1jk,9626
|
|
23
|
+
maps4fs/generator/component/base/component_mesh.py,sha256=plCLS4O8XsVzAtaRi9lm7a8LnS1dcJ7-mqUpVMvCPAg,24704
|
|
24
24
|
maps4fs/generator/component/base/component_xml.py,sha256=MT-VhU2dEckLFxAgmxg6V3gnv11di_94Qq6atfpOLdc,5342
|
|
25
|
-
maps4fs-2.8.
|
|
26
|
-
maps4fs-2.8.
|
|
27
|
-
maps4fs-2.8.
|
|
28
|
-
maps4fs-2.8.
|
|
29
|
-
maps4fs-2.8.
|
|
25
|
+
maps4fs-2.8.3.dist-info/licenses/LICENSE.md,sha256=Ptw8AkqJ60c4tRts6yuqGP_8B0dxwOGmJsp6YJ8dKqM,34328
|
|
26
|
+
maps4fs-2.8.3.dist-info/METADATA,sha256=R4Nh5jeZ3nPZUSZ2KQEG73Sh6mC-MI_jv2_GN7FJw0A,10042
|
|
27
|
+
maps4fs-2.8.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
28
|
+
maps4fs-2.8.3.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
|
29
|
+
maps4fs-2.8.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|