diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index e34eef6..ab70e78 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -26,6 +26,7 @@ import weakref from PyHSPlasma import * from . import utils +from ..helpers import GoodNeighbor class AnimationConverter: def __init__(self, exporter): @@ -36,38 +37,69 @@ class AnimationConverter: return frame_num / self._bl_fps def convert_object_animations(self, bo: bpy.types.Object, so: plSceneObject, anim_name: str, *, - start: Optional[int] = None, end: Optional[int] = None) -> Iterable[plAGApplicator]: + start: Optional[int] = None, end: Optional[int] = None, bake_frame_step: Optional[int] = None) -> Iterable[plAGApplicator]: if not bo.plasma_object.has_animation_data: return [] - def fetch_animation_data(id_data): + temporary_actions = [] + + def bake_animation_data(): + # Baking animations is a Blender operator, so requires a bit of boilerplate... + with GoodNeighbor() as toggle: + # Make sure we have only this object selected. + toggle.track(bo, "hide", False) + for i in bpy.data.objects: + i.select = i == bo + bpy.context.scene.objects.active = bo + + # Do bake, but make sure we don't mess the user's data. + old_action = bo.animation_data.action + try: + frame_start = start if start is not None else bpy.context.scene.frame_start + frame_end = end if end is not None else bpy.context.scene.frame_end + bpy.ops.nla.bake(frame_start=frame_start, frame_end=frame_end, step=bake_frame_step, visual_keying=True, bake_types={"POSE", "OBJECT"}) + action = bo.animation_data.action + finally: + bo.animation_data.action = old_action + temporary_actions.append(action) + return action + + def fetch_animation_data(id_data, can_bake): if id_data is not None: if id_data.animation_data is not None: action = id_data.animation_data.action + if bake_frame_step is not None and can_bake: + action = bake_animation_data() return action, getattr(action, "fcurves", []) return None, [] - obj_action, obj_fcurves = fetch_animation_data(bo) - data_action, data_fcurves = fetch_animation_data(bo.data) - - # We're basically just going to throw all the FCurves at the controller converter (read: wall) - # and see what sticks. PlasmaMAX has some nice animation channel stuff that allows for some - # form of separation, but Blender's NLA editor is way confusing and appears to not work with - # things that aren't the typical position, rotation, scale animations. - applicators = [] - if isinstance(bo.data, bpy.types.Camera): - applicators.append(self._convert_camera_animation(bo, so, obj_fcurves, data_fcurves, anim_name, start, end)) - else: - applicators.append(self._convert_transform_animation(bo, obj_fcurves, bo.matrix_local, bo.matrix_parent_inverse, start=start, end=end)) - if bo.plasma_modifiers.soundemit.enabled: - applicators.extend(self._convert_sound_volume_animation(bo.name, obj_fcurves, bo.plasma_modifiers.soundemit, start, end)) - if isinstance(bo.data, bpy.types.Lamp): - lamp = bo.data - applicators.extend(self._convert_lamp_color_animation(bo.name, data_fcurves, lamp, start, end)) - if isinstance(lamp, bpy.types.SpotLamp): - applicators.extend(self._convert_spot_lamp_animation(bo.name, data_fcurves, lamp, start, end)) - if isinstance(lamp, bpy.types.PointLamp): - applicators.extend(self._convert_omni_lamp_animation(bo.name, data_fcurves, lamp, start, end)) + try: + obj_action, obj_fcurves = fetch_animation_data(bo, True) + data_action, data_fcurves = fetch_animation_data(bo.data, False) + + # We're basically just going to throw all the FCurves at the controller converter (read: wall) + # and see what sticks. PlasmaMAX has some nice animation channel stuff that allows for some + # form of separation, but Blender's NLA editor is way confusing and appears to not work with + # things that aren't the typical position, rotation, scale animations. + applicators = [] + if isinstance(bo.data, bpy.types.Camera): + applicators.append(self._convert_camera_animation(bo, so, obj_fcurves, data_fcurves, anim_name, start, end)) + else: + applicators.append(self._convert_transform_animation(bo, obj_fcurves, bo.matrix_local, bo.matrix_parent_inverse, start=start, end=end)) + if bo.plasma_modifiers.soundemit.enabled: + applicators.extend(self._convert_sound_volume_animation(bo.name, obj_fcurves, bo.plasma_modifiers.soundemit, start, end)) + if isinstance(bo.data, bpy.types.Lamp): + lamp = bo.data + applicators.extend(self._convert_lamp_color_animation(bo.name, data_fcurves, lamp, start, end)) + if isinstance(lamp, bpy.types.SpotLamp): + applicators.extend(self._convert_spot_lamp_animation(bo.name, data_fcurves, lamp, start, end)) + if isinstance(lamp, bpy.types.PointLamp): + applicators.extend(self._convert_omni_lamp_animation(bo.name, data_fcurves, lamp, start, end)) + + finally: + for action in temporary_actions: + # Baking data is temporary, but the lifetime of our user's data is eternal ! + bpy.data.actions.remove(action) return [i for i in applicators if i is not None] diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 8db48fb..e17c9a5 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -99,8 +99,9 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): start, end = min((start, end)), max((start, end)) else: start, end = None, None + bake_frame_step = anim.bake_frame_step if anim.bake else None - applicators = converter.convert_object_animations(bo, so, anim_name, start=start, end=end) + applicators = converter.convert_object_animations(bo, so, anim_name, start=start, end=end, bake_frame_step=bake_frame_step) if not applicators: exporter.report.warn(f"Animation '{anim_name}' generated no applicators. Nothing will be exported.") continue diff --git a/korman/properties/prop_anim.py b/korman/properties/prop_anim.py index 47fb593..f8e0f85 100644 --- a/korman/properties/prop_anim.py +++ b/korman/properties/prop_anim.py @@ -127,6 +127,29 @@ class PlasmaAnimation(bpy.types.PropertyGroup): bpy.types.Texture: "plasma_layer.anim_loop_end", }, }, + "bake": { + "type": BoolProperty, + "property": { + "name": "Bake Keyframes", + "description": "Bake animation keyframes on export. This generates a lot more intermediary keyframes but allows exporting inverse kinematics, and may improve timing for complex animations", + "default": False, + }, + "entire_animation": { + bpy.types.Object: "plasma_modifiers.animation.bake", + }, + }, + "bake_frame_step": { + "type": IntProperty, + "property": { + "name": "Frame step", + "description": "How many frames between each keyframe sample", + "default": 1, + "min": 1, + }, + "entire_animation": { + bpy.types.Object: "plasma_modifiers.animation.bake_frame_step", + }, + }, "sdl_var": { "type": StringProperty, "property": { diff --git a/korman/ui/ui_anim.py b/korman/ui/ui_anim.py index 3741854..c1f2689 100644 --- a/korman/ui/ui_anim.py +++ b/korman/ui/ui_anim.py @@ -67,6 +67,11 @@ def draw_single_animation(layout, anim): col.active = anim.loop and not anim.sdl_var col.prop_search(anim, "loop_start", action, "pose_markers", icon="PMARKER") col.prop_search(anim, "loop_end", action, "pose_markers", icon="PMARKER") + layout.separator() + split = layout.split() + split.prop(anim, "bake") + if anim.bake: + split.prop(anim, "bake_frame_step") layout.separator() layout.prop(anim, "sdl_var")