basic advanced example
This commit is contained in:
1 parent
6fe57915e8
commit
bd8edf5afb
29 files changed
+1501
-38
No files matched your search
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user