more features

This commit is contained in:
Vysyr committed 2026-10-31 23:43:04 +01:00
1 parent bd8edf5afb
commit 1530b74d04
10 files changed
+563 -39

No files matched your search

+103 -16
View File
@@ -43,6 +43,14 @@ class Sequence:
self._root_motion = {}
self._root_motion_copies = {}
self._pose_holds = {}
self._camera_cuts = []
self._visibility = {}
def seconds(self, seconds) -> int:
"""Convert seconds to the nearest frame number at this sequence's FPS."""
if not isinstance(seconds, (int, float)) or not math.isfinite(seconds) or seconds < 0:
raise ValueError("Czas musi byc nieujemna liczba sekund.")
return round(seconds * self.fps)
def bind(self, actor):
key = actor.get_path_name()
@@ -50,17 +58,32 @@ class Sequence:
self._bindings[key] = self.asset.add_possessable(actor)
return self._bindings[key]
def camera(self, actor):
def camera(self, actor, start=0, end=None):
"""Use a camera over [start, end); a later call overrides earlier cameras in its range.
shot.camera(cam_a) followed by shot.camera(cam_b, start=60) cuts at frame 60.
"""
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 kamery: 0 <= start < end <= {self.frames}.")
binding = self.bind(actor)
cuts = [(start, end, binding)]
for old_start, old_end, old_binding in self._camera_cuts:
if old_start < start:
cuts.append((old_start, min(old_end, start), old_binding))
if old_end > end:
cuts.append((max(old_start, end), old_end, old_binding))
self._camera_cuts = sorted(cuts, key=lambda cut: cut[0])
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)
for cut_start, cut_end, cut_binding in self._camera_cuts:
section = track.add_section()
section.set_range(cut_start, cut_end)
binding_id = unreal.MovieSceneObjectBindingID()
binding_id.set_editor_property("guid", cut_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.
@@ -188,6 +211,20 @@ class Sequence:
self._pose_holds[key] = (end, self._hold_pose(track, clip, end, final_time))
return section
def then(self, actor, clip: AnimationClip | str | unreal.AnimSequence | None = None, overlap=0, **options):
"""Play a clip right after the actor's latest clip; overlap frames crossfade them.
Takes the same options as animation(), except start.
"""
if type(overlap) is not int or overlap < 0:
raise ValueError("overlap musi byc nieujemna liczba klatek.")
entry = self.scene._skeletal_models.get(actor.get_path_name())
if entry is None:
raise ValueError("Animacja wymaga aktora utworzonego przez scene.skeletal_model().")
sections = self._animations.get(entry[0].get_path_name())
previous_end = max(item[1] for item in sections) if sections else 0
return self.animation(actor, clip, start=max(previous_end - overlap, 0), **options)
def _hold_pose(self, track, clip, start, time):
section = track.add_section()
params = section.get_editor_property("params")
@@ -257,6 +294,32 @@ class Sequence:
for channel, value in zip(channels[offset:offset + 3], values):
channel.add_key(unreal.FrameNumber(frame), float(value), interpolation=mode)
def visible(self, actor, frame, visible=True):
"""Show or hide an actor and everything attached to it from a frame onward.
The first call decides the earlier state: shot.visible(actor, 60) keeps
the actor hidden before frame 60, shot.visible(actor, 60, False) visible.
"""
if type(frame) is not int or not 0 <= frame < self.frames:
raise ValueError(f"Klatka musi nalezec do 0..{self.frames - 1}.")
if type(visible) is not bool:
raise ValueError("visible musi byc True lub False.")
for part in [actor, *actor.get_attached_actors(True, True)]:
key = part.get_path_name()
if key not in self._visibility:
track = self.bind(part).add_track(unreal.MovieSceneVisibilityTrack)
track.set_property_name_and_path("bHidden", "bHidden")
section = track.add_section()
section.set_range(0, self.frames)
self._visibility[key] = section.get_all_channels()[0]
if frame > 0:
self._visibility[key].add_key(unreal.FrameNumber(0), not visible)
channel = self._visibility[key]
for old in list(channel.get_keys()):
if old.get_time().frame_number.value == frame:
channel.remove_key(old)
channel.add_key(unreal.FrameNumber(frame), visible)
def light_key(self, actor, frame, color=None, intensity=None, interpolation="linear"):
"""Key a light's color (linear RGB 0-1) and/or intensity at a sequence frame."""
if not isinstance(frame, int) or not 0 <= frame < self.frames:
@@ -294,16 +357,27 @@ class Sequence:
Evaluates Sequencer curves without moving the playhead. Does not include
motion of bones inside an animation that was not transferred to Transform.
"""
return self._read_transform(actor, frame, 0, lambda: actor.get_actor_location().to_tuple())
def rotation(self, actor, frame: int) -> tuple[float, float, float]:
"""Actor (roll, pitch, yaw) at a sequence frame, evaluated like position()."""
def initial():
rotation = actor.get_actor_rotation()
return (rotation.roll, rotation.pitch, rotation.yaw)
return self._read_transform(actor, frame, 3, initial)
def _read_transform(self, actor, frame, first_channel, initial):
if type(frame) is not int or not 0 <= frame < self.frames:
raise ValueError(f"Klatka musi nalezec do 0..{self.frames - 1}.")
channels = self._transforms.get(actor.get_path_name())
if channels is None:
return actor.get_actor_location().to_tuple()
return initial()
interval = self.asset.make_range(frame, 1)
rate = unreal.FrameRate(self.fps, 1)
values = [channel.evaluate_keys(interval, rate) for channel in channels[:3]]
values = [channel.evaluate_keys(interval, rate)
for channel in channels[first_channel:first_channel + 3]]
if any(not axis for axis in values):
raise RuntimeError("Nie mozna odczytac pozycji aktora z kanalow Transform.")
raise RuntimeError("Nie mozna odczytac aktora z kanalow Transform.")
return tuple(float(axis[0]) for axis in values)
def move(self, actor, start: int, end: int, offset: tuple[float, float, float],
@@ -314,19 +388,32 @@ class Sequence:
the resulting change, preserving later motion. Call after animation().
Returns the target position. Rotation/scale keys are kept.
"""
return self._relative_keys(actor, start, end, offset, interpolation, "location", 0, self.position)
def rotate(self, actor, start: int, end: int, offset: tuple[float, float, float],
interpolation="auto") -> tuple[float, float, float]:
"""Rotate by offset (roll, pitch, yaw degrees) from the actor's rotation at start.
Works like move() for rotation keys; offset yaw=360 is one full turn.
keep_end_position still follows the direction from before the rotation.
Returns the target rotation. Position/scale keys are kept.
"""
return self._relative_keys(actor, start, end, offset, interpolation, "rotation", 3, self.rotation)
def _relative_keys(self, actor, start, end, offset, interpolation, keyword, first_channel, read):
if (type(start) is not int or type(end) is not int or
not 0 <= start < end < self.frames):
raise ValueError(f"Ruch wymaga 0 <= start < end < {self.frames}.")
raise ValueError(f"Zakres wymaga 0 <= start < end < {self.frames}.")
if interpolation not in {"linear", "auto", "constant"}:
raise ValueError("Interpolacja: linear, auto lub constant.")
if len(offset) != 3 or any(not math.isfinite(value) for value in offset):
raise ValueError("offset wymaga trzech skonczonych wartosci w centymetrach.")
origin = self.position(actor, start)
raise ValueError("offset wymaga trzech skonczonych wartosci.")
origin = read(actor, start)
target = tuple(value + delta for value, delta in zip(origin, offset))
channels = self._transforms.get(actor.get_path_name())
if channels is not None:
previous_end = self.position(actor, end)
for axis, channel in enumerate(channels[:3]):
previous_end = read(actor, end)
for axis, channel in enumerate(channels[first_channel:first_channel + 3]):
correction = target[axis] - previous_end[axis]
for key in list(channel.get_keys()):
time = key.get_time().frame_number.value
@@ -334,8 +421,8 @@ class Sequence:
channel.remove_key(key)
elif time > end:
key.set_value(key.get_value() + correction)
self.transform_key(actor, start, location=origin, interpolation=interpolation)
self.transform_key(actor, end, location=target, interpolation=interpolation)
self.transform_key(actor, start, interpolation=interpolation, **{keyword: origin})
self.transform_key(actor, end, interpolation=interpolation, **{keyword: target})
return target
def save(self, open_editor=True):