Files
PythonInUnreal/Content/Python/ue_workshop/scene.py
T
2026-10-31 20:16:32 +01:00

165 lines
9.3 KiB
Python

"""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.
"""
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)