165 lines
9.3 KiB
Python
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)
|