"""An isolated generated map, simple meshes and a ready-to-use character.""" import math from pathlib import Path import unreal from .paths import GENERATED, PROJECT, slug class Scene: """Owns one map. Building again replaces only that generated map's actors.""" def __init__(self, name="MyShot"): from . import render if render.is_rendering() or unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem).is_rendering(): raise RuntimeError("Poczekaj na zakonczenie renderu przed przebudowa sceny.") # Discard Sequencer's live bindings before replacing their actors. current_sequence = unreal.LevelSequenceEditorBlueprintLibrary.get_current_level_sequence() if current_sequence and current_sequence.get_path_name().startswith(GENERATED + "/"): unreal.LevelSequenceEditorBlueprintLibrary.close_level_sequence() self.name = slug(name) self._skeletal_models = {} self.map_path = f"{GENERATED}/{name}/Map" self.subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) editor = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) current = editor.get_current_level() current_path = current.get_outer().get_path_name().split(".")[0] if current else "" if current_path == self.map_path: # This map is wholly generated by this exercise, never a user's other map. for actor in self.subsystem.get_all_level_actors(): if "Workshop" in [str(tag) for tag in actor.tags]: self.subsystem.destroy_actor(actor) else: if unreal.EditorLoadingAndSavingUtils.get_dirty_map_packages(): raise RuntimeError("Zapisz zmiany aktualnej mapy przed utworzeniem sceny warsztatowej.") if unreal.EditorAssetLibrary.does_asset_exist(self.map_path): if not editor.load_level(self.map_path): raise RuntimeError("Nie mozna otworzyc " + self.map_path) for actor in self.subsystem.get_all_level_actors(): if "Workshop" in [str(tag) for tag in actor.tags]: self.subsystem.destroy_actor(actor) elif not editor.new_level(self.map_path): raise RuntimeError("Nie mozna utworzyc " + self.map_path) def spawn(self, actor_class, name, location=(0, 0, 0), rotation=(0, 0, 0)): actor = self.subsystem.spawn_actor_from_class(actor_class, unreal.Vector(*location), unreal.Rotator(*rotation)) if actor is None: raise RuntimeError("Nie mozna utworzyc aktora " + name) actor.set_actor_label(name) actor.tags = ["Workshop"] actor.set_folder_path("Workshop") return actor def mesh(self, asset, name="Model", location=(0, 0, 0), scale=(1, 1, 1), material=None): mesh = unreal.load_asset(asset) if isinstance(asset, str) else asset if not isinstance(mesh, unreal.StaticMesh): raise TypeError(f"Oczekiwano StaticMesh: {asset}") actor = self.spawn(unreal.StaticMeshActor, name, location) actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE) actor.static_mesh_component.set_static_mesh(mesh) actor.set_actor_scale3d(unreal.Vector(*scale)) if material is not None: actor.static_mesh_component.set_material(0, material) return actor def primitive(self, shape="Cube", **kwargs): if shape not in {"Cube", "Sphere", "Cylinder", "Cone", "Plane"}: raise ValueError("Ksztalt: Cube, Sphere, Cylinder, Cone lub Plane.") return self.mesh(f"/Engine/BasicShapes/{shape}.{shape}", **kwargs) def model(self, filename, name="Character", height=200, rotation=(0, 0, 0), reimport=False): """Import a model, fit its height (cm), and give it a floor-centered pivot. Returns a parent actor for transform animation; imported materials stay on its mesh child. Rotation is (roll, pitch, yaw), applied before fitting. """ if height is not None and (not isinstance(height, (int, float)) or not math.isfinite(height) or height <= 0): raise ValueError("Wysokosc modelu musi byc dodatnia; None zachowuje rozmiar pliku.") source = Path(filename).expanduser() source = source if source.is_absolute() else PROJECT / source if source.suffix.lower() == ".blend": from .blender import import_character mesh = import_character(source, name=name, reimport=reimport) else: from .assets import import_model mesh = import_model(source, name=name, reimport=reimport) actor = self.mesh(mesh, name=name + "Mesh") roll, pitch, yaw = rotation actor.set_actor_rotation(unreal.Rotator(roll=roll, pitch=pitch, yaw=yaw), False) origin, extent = actor.get_actor_bounds(False) if extent.z <= 0: self.subsystem.destroy_actor(actor) raise ValueError("Model ma zerowa wysokosc. Sprawdz geometrie i MODEL_ROTATION.") if height is not None: factor = float(height) / (2 * extent.z) actor.set_actor_scale3d(unreal.Vector(factor, factor, factor)) origin, extent = actor.get_actor_bounds(False) actor.set_actor_location(unreal.Vector(-origin.x, -origin.y, extent.z - origin.z), False, False) root = self.spawn(unreal.StaticMeshActor, name) root.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE) actor.attach_to_actor(root, "", unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, False) return root def skeletal_model(self, filename, name="Character", height=200, rotation=(0, 0, 0), location=(0, 0, 0), animations=None, collection="Character", reimport=False): """Import an animated FBX/.blend; return a parent actor for transform keys. By default all animation FBXs beside the model are available by filename: shot.animation(actor, "great_sword_walk"). Optional animations={...} selects specific files; animations={} keeps only embedded animations. Also writes NameAnimations with durations to ue_workshop.animations for editor completion, including custom models (name="Ninja" -> NinjaAnimations). """ from .skeletal import import_model if height is not None and (not isinstance(height, (int, float)) or not math.isfinite(height) or height <= 0): raise ValueError("Wysokosc modelu musi byc dodatnia; None zachowuje rozmiar pliku.") imported = import_model(filename, name, animations, collection, reimport=reimport) child = self.spawn(unreal.SkeletalMeshActor, name + "Mesh") component = child.skeletal_mesh_component component.set_mobility(unreal.ComponentMobility.MOVABLE) component.set_skeletal_mesh_asset(imported.mesh) component.set_update_animation_in_editor(True) component.set_editor_property("visibility_based_anim_tick_option", unreal.VisibilityBasedAnimTickOption.ALWAYS_TICK_POSE_AND_REFRESH_BONES) roll, pitch, yaw = rotation child.set_actor_rotation(unreal.Rotator(roll=roll, pitch=pitch, yaw=yaw), False) origin, extent = child.get_actor_bounds(False) if extent.z <= 0: self.subsystem.destroy_actor(child) raise ValueError("Model ma zerowa wysokosc. Sprawdz geometrie i rotacje.") if height is not None: factor = float(height) / (2 * extent.z) child.set_actor_scale3d(unreal.Vector(factor, factor, factor)) origin, extent = child.get_actor_bounds(False) child.set_actor_location(unreal.Vector(-origin.x, -origin.y, extent.z - origin.z), False, False) root = self.spawn(unreal.StaticMeshActor, name) root.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE) child.attach_to_actor(root, "", unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, False) root.set_actor_location(unreal.Vector(*location), False, False) self._skeletal_models[root.get_path_name()] = (child, imported) return root def character(self, material=None, head_scale=1.0): root = self.spawn(unreal.StaticMeshActor, "Character") root.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE) for shape, name, position, scale in [ ("Cube", "Body", (0, 0, 90), (0.55, 0.8, 0.9)), ("Sphere", "Head", (0, 0, 175), (0.7 * head_scale,) * 3), ("Cube", "LeftLeg", (0, -23, 25), (0.35, 0.3, 0.5)), ("Cube", "RightLeg", (0, 23, 25), (0.35, 0.3, 0.5)), ("Sphere", "Nose", (34, 0, 177), (0.24, 0.2, 0.2)), ]: actor = self.primitive(shape, name=name, location=position, scale=scale, material=material) actor.attach_to_actor(root, "", unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, False) return root def save(self): editor = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) if not editor.save_current_level(): raise RuntimeError("Nie mozna zapisac mapy " + self.map_path)