basic advanced example

This commit is contained in:
Vysyr committed 2026-10-31 23:11:57 +01:00
1 parent 6fe57915e8
commit bd8edf5afb
29 files changed
+1501 -38

No files matched your search

+194 -9
View File
@@ -4,6 +4,16 @@ import math
import unreal
from .paths import GENERATED, slug
from .animation_clip import AnimationClip
def _key_mode(interpolation):
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.")
return modes[interpolation]
class Sequence:
@@ -28,7 +38,11 @@ class Sequence:
self.asset.set_playback_end(frames)
self._bindings = {}
self._transforms = {}
self._light_tracks = {}
self._animations = {}
self._root_motion = {}
self._root_motion_copies = {}
self._pose_holds = {}
def bind(self, actor):
key = actor.get_path_name()
@@ -66,19 +80,41 @@ class Sequence:
weight.add_key(unreal.FrameNumber(frame), value,
interpolation=unreal.MovieSceneKeyInterpolation.LINEAR)
def animation(self, actor, clip=None, start=0, end=None, speed=1.0):
def animation(self, actor, clip: AnimationClip | str | unreal.AnimSequence | None = None,
start=0, end=None, speed=1.0, keep_end_position=False, reverse=False,
keep_end_pose=False):
"""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.
Pass a FighterAnimations/CreatureAnimations member, a filename key, an
AnimSequence, or None for the only available animation. With an enum and
no end, play once using its stored duration, sequence FPS and speed.
Strings/AnimSequence/None keep looping to the sequence end if end is omitted.
An explicit end sets the section range and can repeat the clip.
Overlapping clips automatically crossfade over their entire shared range.
Add clips in timeline order; at most two clips can overlap at a time.
keep_end_position transfers horizontal root travel into actor Transform
keys, keeping the final position for subsequent clips. Defaults to False.
reverse=True plays the clip backwards, including transferred root travel.
keep_end_pose=True retains the final pose with a frozen section after
playback and Sequencer Keep State. A later clip ends this pose hold.
"""
if type(start) is not int:
raise ValueError("start musi byc numerem klatki (liczba calkowita).")
if isinstance(clip, AnimationClip):
if end is None:
end = start + clip.frames(self.fps, speed)
clip = clip.key
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.")
if type(keep_end_position) is not bool:
raise ValueError("keep_end_position musi byc True lub False.")
if type(reverse) is not bool:
raise ValueError("reverse musi byc True lub False.")
if type(keep_end_pose) is not bool:
raise ValueError("keep_end_pose musi byc True lub False.")
entry = self.scene._skeletal_models.get(actor.get_path_name())
if entry is None:
raise ValueError("Animacja wymaga aktora utworzonego przez scene.skeletal_model().")
@@ -109,29 +145,97 @@ class Sequence:
previous = overlaps[0]
blend_frames = previous[1] - start
track = sections[0][2] if sections else self.bind(child).add_track(unreal.MovieSceneSkeletalAnimationTrack)
hold = self._pose_holds.pop(key, None)
if hold is not None:
hold_start, hold_section = hold
if start <= hold_start:
track.remove_section(hold_section)
else:
hold_section.set_range(hold_start, start)
if keep_end_position:
from .root_motion import prepare
source_key = clip.get_path_name()
if source_key not in self._root_motion_copies:
self._root_motion_copies[source_key] = prepare(
clip, child, self.scene.name + "/" + self.asset.get_name())
clip, poses, duration = self._root_motion_copies[source_key]
section = track.add_section()
section.set_range(start, end)
params = section.get_editor_property("params")
params.set_editor_property("animation", clip)
params.set_editor_property("reverse", reverse)
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)
# UE may rescale the range when play_rate changes. Restore the requested
# display-frame bounds after applying parameters (including speed).
section.set_range(start, end)
if keep_end_pose:
section.set_completion_mode(unreal.MovieSceneCompletionMode.KEEP_STATE)
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))
if keep_end_position:
self._keep_end_position(actor, child, start, end, speed, poses, duration, reverse)
if keep_end_pose and end < self.frames:
length = clip.get_play_length()
final_time = min((end - start) / self.fps * speed, length)
if reverse:
final_time = length - final_time
self._pose_holds[key] = (end, self._hold_pose(track, clip, end, final_time))
return section
def _hold_pose(self, track, clip, start, time):
section = track.add_section()
params = section.get_editor_property("params")
params.set_editor_property("animation", clip)
rate = unreal.MovieSceneTimeWarpVariant()
rate.set_fixed_play_rate(0.0)
params.set_editor_property("play_rate", rate)
ticks = self.asset.get_tick_resolution()
tick_rate = ticks.numerator / ticks.denominator
# Stay just inside the asset end to avoid modulo wrapping to frame zero.
time = max(0.0, min(time, clip.get_play_length() - 1 / tick_rate))
params.set_editor_property("first_loop_start_frame_offset", unreal.FrameNumber(round(time * tick_rate)))
params.set_editor_property("force_custom_mode", True)
section.set_editor_property("params", params)
section.set_range(start, self.frames)
section.set_completion_mode(unreal.MovieSceneCompletionMode.KEEP_STATE)
self._animation_blend(section, start, self.frames, 0, 0)
return section
def _keep_end_position(self, actor, child, start, end, speed, poses, duration, reverse=False):
from .root_motion import displacement
key = actor.get_path_name()
if key not in self._root_motion:
self._root_motion[key] = {
"origin": actor.get_actor_location(),
"mesh_transform": child.get_actor_transform(),
"clips": [],
}
motion = self._root_motion[key]
motion["clips"].append((start, end, speed, poses, duration, reverse))
mesh = motion["mesh_transform"]
zero = mesh.transform_location(unreal.Vector(0, 0, 0))
# These are ordinary Transform keys. The final key holds the position
# after playback; subsequent animation calls do not reset it.
first = min(item[0] for item in motion["clips"])
last = min(self.frames - 1, max(item[1] for item in motion["clips"]))
for frame in range(first, last + 1):
total = unreal.Vector(0, 0, 0)
for clip_start, clip_end, clip_speed, clip_poses, clip_duration, clip_reverse in motion["clips"]:
elapsed = min(max(frame - clip_start, 0), clip_end - clip_start) / self.fps * clip_speed
total += displacement(clip_poses, clip_duration, elapsed, reverse=clip_reverse)
world_delta = mesh.transform_location(total) - zero
self.transform_key(actor, frame, location=(motion["origin"] + world_delta).to_tuple())
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.")
mode = _key_mode(interpolation)
key = actor.get_path_name()
if key not in self._transforms:
track = self.bind(actor).add_track(unreal.MovieScene3DTransformTrack)
@@ -151,7 +255,88 @@ class Sequence:
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])
channel.add_key(unreal.FrameNumber(frame), float(value), interpolation=mode)
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:
raise ValueError(f"Klatka musi nalezec do 0..{self.frames - 1}.")
mode = _key_mode(interpolation)
if color is not None and len(color) != 3:
raise ValueError("Kolor wymaga trzech wartosci.")
light = actor.get_component_by_class(unreal.LightComponent)
if light is None:
raise ValueError("light_key wymaga aktora swiatla, np. z lighting.spot().")
if color is not None:
channels = self._light_channels(light, unreal.MovieSceneColorTrack, "LightColor")
for channel, value in zip(channels, color):
channel.add_key(unreal.FrameNumber(frame), float(value), interpolation=mode)
if intensity is not None:
channels = self._light_channels(light, unreal.MovieSceneFloatTrack, "Intensity")
channels[0].add_key(unreal.FrameNumber(frame), float(intensity), interpolation=mode)
def _light_channels(self, light, track_class, name):
key = (light.get_path_name(), name)
if key not in self._light_tracks:
track = self.bind(light).add_track(track_class)
track.set_property_name_and_path(name, name)
section = track.add_section()
section.set_range(0, self.frames)
channels = section.get_all_channels()
if name == "LightColor":
channels[3].set_default(1.0) # Alpha is never keyed.
self._light_tracks[key] = channels
return self._light_tracks[key]
def position(self, actor, frame: int) -> tuple[float, float, float]:
"""Actor Transform position at a sequence frame, including keep_end_position.
Evaluates Sequencer curves without moving the playhead. Does not include
motion of bones inside an animation that was not transferred to Transform.
"""
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()
interval = self.asset.make_range(frame, 1)
rate = unreal.FrameRate(self.fps, 1)
values = [channel.evaluate_keys(interval, rate) for channel in channels[:3]]
if any(not axis for axis in values):
raise RuntimeError("Nie mozna odczytac pozycji 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],
interpolation="auto") -> tuple[float, float, float]:
"""Move by offset (world cm) from the actor's position at start.
Replaces position keys in the range and shifts later position keys by
the resulting change, preserving later motion. Call after animation().
Returns the target position. Rotation/scale keys are kept.
"""
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}.")
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)
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]):
correction = target[axis] - previous_end[axis]
for key in list(channel.get_keys()):
time = key.get_time().frame_number.value
if start <= time <= end:
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)
return target
def save(self, open_editor=True):
self.scene.save()