more features
This commit is contained in:
1 parent
bd8edf5afb
commit
1530b74d04
10 files changed
+563
-39
No files matched your search
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user