161 lines
8.2 KiB
Python
161 lines
8.2 KiB
Python
"""Frame-based Sequencer helpers. Ranges are [start, end), rotations roll/pitch/yaw."""
|
|
import math
|
|
|
|
import unreal
|
|
|
|
from .paths import GENERATED, slug
|
|
|
|
|
|
class Sequence:
|
|
def __init__(self, scene, name="Shot", fps=24, frames=48):
|
|
if not isinstance(fps, int) or fps < 1 or not isinstance(frames, int) or frames < 1:
|
|
raise ValueError("FPS i liczba klatek musza byc dodatnimi liczbami calkowitymi.")
|
|
self.scene, self.fps, self.frames = scene, fps, frames
|
|
path = f"{GENERATED}/{scene.name}/LS_{slug(name)}"
|
|
self.asset = unreal.load_asset(path)
|
|
if self.asset and unreal.LevelSequenceEditorBlueprintLibrary.get_current_level_sequence() == self.asset:
|
|
unreal.LevelSequenceEditorBlueprintLibrary.close_level_sequence()
|
|
if self.asset is None:
|
|
self.asset = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
|
"LS_" + name, f"{GENERATED}/{scene.name}", unreal.LevelSequence,
|
|
unreal.LevelSequenceFactoryNew())
|
|
for binding in self.asset.get_bindings():
|
|
binding.remove()
|
|
for track in self.asset.get_tracks():
|
|
self.asset.remove_track(track)
|
|
self.asset.set_display_rate(unreal.FrameRate(fps, 1))
|
|
self.asset.set_playback_start(0)
|
|
self.asset.set_playback_end(frames)
|
|
self._bindings = {}
|
|
self._transforms = {}
|
|
self._animations = {}
|
|
|
|
def bind(self, actor):
|
|
key = actor.get_path_name()
|
|
if key not in self._bindings:
|
|
self._bindings[key] = self.asset.add_possessable(actor)
|
|
return self._bindings[key]
|
|
|
|
def camera(self, actor):
|
|
binding = self.bind(actor)
|
|
tracks = self.asset.find_tracks_by_type(unreal.MovieSceneCameraCutTrack)
|
|
track = tracks[0] if tracks else self.asset.add_track(unreal.MovieSceneCameraCutTrack)
|
|
for old in track.get_sections():
|
|
track.remove_section(old)
|
|
section = track.add_section()
|
|
section.set_range(0, self.frames)
|
|
binding_id = unreal.MovieSceneObjectBindingID()
|
|
binding_id.set_editor_property("guid", binding.get_id())
|
|
section.set_camera_binding_id(binding_id)
|
|
|
|
def _animation_blend(self, section, start, end, fade_in, fade_out):
|
|
# The skeletal animation section exposes its Weight channel to Python.
|
|
# add_key uses display frames and converts them to the sequence tick rate.
|
|
weight = section.get_all_channels()[0]
|
|
for key in weight.get_keys():
|
|
weight.remove_key(key)
|
|
weight.set_default(1.0)
|
|
if fade_in or fade_out:
|
|
keys = {start: 0.0 if fade_in else 1.0}
|
|
if fade_in:
|
|
keys[start + fade_in] = 1.0
|
|
if fade_out:
|
|
keys[end - fade_out] = 1.0
|
|
keys[end] = 0.0
|
|
for frame, value in sorted(keys.items()):
|
|
weight.add_key(unreal.FrameNumber(frame), value,
|
|
interpolation=unreal.MovieSceneKeyInterpolation.LINEAR)
|
|
|
|
def animation(self, actor, clip=None, start=0, end=None, speed=1.0):
|
|
"""Play/loop an imported clip over [start, end) in sequence frames.
|
|
|
|
clip is an alias from scene.skeletal_model(animations=...), an AnimSequence,
|
|
or None for the only embedded animation. Transform keys still move the parent.
|
|
Overlapping clips automatically crossfade over their entire shared range.
|
|
Add clips in timeline order; at most two clips can overlap at a time.
|
|
"""
|
|
end = self.frames if end is None else end
|
|
if type(start) is not int or type(end) is not int or not 0 <= start < end <= self.frames:
|
|
raise ValueError(f"Zakres animacji: 0 <= start < end <= {self.frames}.")
|
|
if not isinstance(speed, (int, float)) or not math.isfinite(speed) or speed <= 0:
|
|
raise ValueError("Predkosc animacji musi byc dodatnia.")
|
|
entry = self.scene._skeletal_models.get(actor.get_path_name())
|
|
if entry is None:
|
|
raise ValueError("Animacja wymaga aktora utworzonego przez scene.skeletal_model().")
|
|
child, imported = entry
|
|
if clip is None:
|
|
if len(imported.animations) != 1:
|
|
raise ValueError("Wybierz animacje: " + ", ".join(imported.animations))
|
|
clip = next(iter(imported.animations.values()))
|
|
elif isinstance(clip, str):
|
|
if clip not in imported.animations:
|
|
raise ValueError("Nieznana animacja. Dostepne: " + ", ".join(imported.animations))
|
|
clip = imported.animations[clip]
|
|
if not isinstance(clip, unreal.AnimSequence):
|
|
raise TypeError("clip musi byc nazwa zaimportowanej animacji lub AnimSequence.")
|
|
if clip.get_editor_property("skeleton") != imported.mesh.get_editor_property("skeleton"):
|
|
raise ValueError("Animacja i model musza uzywac tego samego szkieletu.")
|
|
key = child.get_path_name()
|
|
sections = self._animations.setdefault(key, [])
|
|
overlaps = [old for old in sections if start < old[1] and old[0] < end]
|
|
previous = None
|
|
blend_frames = 0
|
|
if overlaps:
|
|
if (len(overlaps) != 1 or not overlaps[0][0] < start or
|
|
overlaps[0][1] >= end or
|
|
overlaps[0][0] + overlaps[0][4] > start):
|
|
raise ValueError("Dodawaj klipy w kolejnosci odtwarzania. Blend wymaga czesciowego "
|
|
"nakladania dwoch klipow, bez trzech jednoczesnych animacji.")
|
|
previous = overlaps[0]
|
|
blend_frames = previous[1] - start
|
|
track = sections[0][2] if sections else self.bind(child).add_track(unreal.MovieSceneSkeletalAnimationTrack)
|
|
section = track.add_section()
|
|
section.set_range(start, end)
|
|
params = section.get_editor_property("params")
|
|
params.set_editor_property("animation", clip)
|
|
rate = unreal.MovieSceneTimeWarpVariant()
|
|
rate.set_fixed_play_rate(float(speed))
|
|
params.set_editor_property("play_rate", rate)
|
|
params.set_editor_property("force_custom_mode", True)
|
|
section.set_editor_property("params", params)
|
|
self._animation_blend(section, start, end, blend_frames, 0)
|
|
if previous:
|
|
self._animation_blend(previous[3], previous[0], previous[1], previous[4], blend_frames)
|
|
sections.append((start, end, track, section, blend_frames))
|
|
return section
|
|
|
|
def transform_key(self, actor, frame, location=None, rotation=None, scale=None, interpolation="linear"):
|
|
if not isinstance(frame, int) or not 0 <= frame < self.frames:
|
|
raise ValueError(f"Klatka musi nalezec do 0..{self.frames - 1}.")
|
|
modes = {"linear": unreal.MovieSceneKeyInterpolation.LINEAR,
|
|
"auto": unreal.MovieSceneKeyInterpolation.AUTO,
|
|
"constant": unreal.MovieSceneKeyInterpolation.CONSTANT}
|
|
if interpolation not in modes:
|
|
raise ValueError("Interpolacja: linear, auto lub constant.")
|
|
key = actor.get_path_name()
|
|
if key not in self._transforms:
|
|
track = self.bind(actor).add_track(unreal.MovieScene3DTransformTrack)
|
|
section = track.add_section()
|
|
section.set_range(0, self.frames)
|
|
channels = section.get_all_channels()
|
|
initial = actor.get_actor_transform()
|
|
r = initial.rotation.rotator()
|
|
defaults = [*initial.translation.to_tuple(), r.roll, r.pitch, r.yaw,
|
|
*initial.scale3d.to_tuple()]
|
|
for channel, value in zip(channels, defaults):
|
|
channel.set_default(float(value))
|
|
self._transforms[key] = channels
|
|
channels = self._transforms[key]
|
|
for offset, values in [(0, location), (3, rotation), (6, scale)]:
|
|
if values is not None:
|
|
if len(values) != 3:
|
|
raise ValueError("Transformacja wymaga trzech wartosci.")
|
|
for channel, value in zip(channels[offset:offset + 3], values):
|
|
channel.add_key(unreal.FrameNumber(frame), float(value), interpolation=modes[interpolation])
|
|
|
|
def save(self, open_editor=True):
|
|
self.scene.save()
|
|
unreal.EditorAssetLibrary.save_loaded_asset(self.asset)
|
|
if open_editor:
|
|
unreal.LevelSequenceEditorBlueprintLibrary.open_level_sequence(self.asset)
|