From 5b5577f6f5c90bde26d6cd47b3dcba7927ec495b Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 4 Jul 2015 20:29:00 -0400 Subject: [PATCH 1/3] Implement rudimentary object animation --- korman/exporter/__init__.py | 1 + korman/exporter/animation.py | 119 ++++++++++++++++ korman/exporter/convert.py | 2 + korman/operators/op_modifier.py | 41 ++++++ korman/properties/modifiers/__init__.py | 1 + korman/properties/modifiers/anim.py | 172 ++++++++++++++++++++++++ korman/ui/modifiers/__init__.py | 1 + korman/ui/modifiers/anim.py | 70 ++++++++++ 8 files changed, 407 insertions(+) create mode 100644 korman/exporter/animation.py create mode 100644 korman/properties/modifiers/anim.py create mode 100644 korman/ui/modifiers/anim.py diff --git a/korman/exporter/__init__.py b/korman/exporter/__init__.py index 8623197..3eb954d 100644 --- a/korman/exporter/__init__.py +++ b/korman/exporter/__init__.py @@ -19,3 +19,4 @@ from PyHSPlasma import * from .convert import * from .explosions import * +from . import utils diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py new file mode 100644 index 0000000..c14102f --- /dev/null +++ b/korman/exporter/animation.py @@ -0,0 +1,119 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +import math +from PyHSPlasma import * +import weakref + +class AnimationConverter: + def __init__(self, exporter): + self._exporter = weakref.ref(exporter) + self._bl_fps = bpy.context.scene.render.fps + + def _check_scalar_subcontrollers(self, ctrl, default_xform): + """Ensures that all scalar subcontrollers have at least one keyframe in the default state""" + for i in ("X", "Y", "Z"): + sub = getattr(ctrl, i) + if not sub.hasKeys(): + keyframe = hsScalarKey() + keyframe.frame = 0 + keyframe.frameTime = 0.0 + keyframe.type = hsKeyFrame.kScalarKeyFrame + keyframe.value = getattr(default_xform, i.lower()) + sub.keys = ([keyframe,], hsKeyFrame.kScalarKeyFrame) + + def convert_action2tm(self, action, default_xform): + """Converts a Blender Action to a plCompoundController.""" + fcurves = action.fcurves + if not fcurves: + return None + + # NOTE: plCompoundController is from Myst 5 and was backported to MOUL. + # Worry not however... libHSPlasma will do the conversion for us. + tm = plCompoundController() + tm.X = self.make_pos_controller(fcurves, default_xform) + tm.Y = self.make_rot_controller(fcurves, default_xform) + tm.Z = self.make_scale_controller(fcurves, default_xform) + return tm + + def _is_bezier_curve(self, keyframes): + for i in keyframes: + if i.interpolation == "BEZIER": + return True + return False + + def make_pos_controller(self, fcurves, default_xform): + pos_curves = (i for i in fcurves if i.data_path == "location" and i.keyframe_points) + ctrl = self.make_scalar_controller(pos_curves) + if ctrl is not None and default_xform is not None: + self._check_scalar_subcontrollers(ctrl, default_xform.to_transpose()) + + def make_rot_controller(self, fcurves, default_xform): + rot_curves = (i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points) + ctrl = self.make_scalar_controller(rot_curves) + if ctrl is not None and default_xform is not None: + self._check_scalar_subcontrollers(ctrl, default_xform.to_euler("XYZ")) + return ctrl + + def make_scale_controller(self, fcurves, default_xform): + # ... TODO ... + # who needs this anyway? + return None + + def make_scalar_controller(self, fcurves): + ctrl = plCompoundController() + subctls = ("X", "Y", "Z") + + # this ensures that all subcontrollers are populated -- otherwise KABLOOEY! + for i in subctls: + setattr(ctrl, i, plLeafController()) + + for fcurve in fcurves: + fcurve.update() + if self._is_bezier_curve(fcurve.keyframe_points): + key_type = hsKeyFrame.kScalarKeyFrame + else: + key_type = hsKeyFrame.kBezScalarKeyFrame + frames = [] + pi = math.pi + fps = self._bl_fps + + for i in fcurve.keyframe_points: + bl_frame_num, value = i.co + frame = hsScalarKey() + if i.interpolation == "BEZIER": + frame.inTan = -(value - i.handle_left[1]) / (bl_frame_num - i.handle_left[0]) / fps / (2 * pi) + frame.outTan = (value - i.handle_right[1]) / (bl_frame_num - i.handle_right[0]) / fps / (2 * pi) + else: + frame.inTan = 0.0 + frame.outTan = 0.0 + frame.type = key_type + frame.frame = int(bl_frame_num * (30.0 / fps)) + frame.frameTime = bl_frame_num / fps + frame.value = value + frames.append(frame) + controller = plLeafController() + getattr(ctrl, subctls[fcurve.array_index]).keys = (frames, key_type) + + # Compact this bamf + if not ctrl.X.hasKeys() and not ctrl.Y.hasKeys() and not ctrl.Z.hasKeys(): + return None + else: + return ctrl + + @property + def _mgr(self): + return self._exporter().mgr diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 5a907d0..94e8f96 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -18,6 +18,7 @@ import os.path from PyHSPlasma import * import time +from . import animation from . import explosions from . import logger from . import manager @@ -47,6 +48,7 @@ class Exporter: self.report = logger.ExportAnalysis() self.physics = physics.PhysicsConverter(self) self.light = rtlight.LightConverter(self) + self.animation = animation.AnimationConverter(self) self.sumfile = sumfile.SumFile() # Step 1: Create the age info and the pages diff --git a/korman/operators/op_modifier.py b/korman/operators/op_modifier.py index 6c4e882..bd774f7 100644 --- a/korman/operators/op_modifier.py +++ b/korman/operators/op_modifier.py @@ -148,3 +148,44 @@ class ModifierLogicWizOperator(ModifierOperator, bpy.types.Operator): end = time.process_time() print("\nLogicWiz finished in {:.2f} seconds".format(end-start)) return {"FINISHED"} + + +class ModifierCollectionAddOperator(ModifierOperator, bpy.types.Operator): + bl_idname = "object.plasma_modifier_collection_add" + bl_label = "Add Item" + bl_description = "Adds an item to the collection" + + modifier = StringProperty(name="Modifier", description="Attribute name of the Plasma Modifier") + collection = StringProperty(name="Collection", description="Attribute name of the collection property") + name_prefix = StringProperty(name="Name Prefix", description="Prefix for autogenerated item names", default="Item") + name_prop = StringProperty(name="Name Property", description="Attribute name of the item name property") + + def execute(self, context): + obj = context.active_object + mod = getattr(obj.plasma_modifiers, self.modifier) + collection = getattr(mod, self.collection) + idx = len(collection) + collection.add() + if self.name_prop: + setattr(collection[idx], self.name_prop, "{} {}".format(self.name_prefix, idx+1)) + return {"FINISHED"} + + +class ModifierCollectionRemoveOperator(ModifierOperator, bpy.types.Operator): + bl_idname = "object.plasma_modifier_collection_remove" + bl_label = "Remove Item" + bl_description = "Removes an item from the collection" + + modifier = StringProperty(name="Modifier", description="Attribute name of the Plasma Modifier") + collection = StringProperty(name="Collection", description="Attribute name of the collection property") + index = IntProperty(name="Index", description="Item index to remove") + + def execute(self, context): + obj = context.active_object + mod = getattr(obj.plasma_modifiers, self.modifier) + collection = getattr(mod, self.collection) + if len(collection) > self.index: + collection.remove(self.index) + return {"FINISHED"} + else: + return {"CANCELLED"} diff --git a/korman/properties/modifiers/__init__.py b/korman/properties/modifiers/__init__.py index 25897be..aa3c739 100644 --- a/korman/properties/modifiers/__init__.py +++ b/korman/properties/modifiers/__init__.py @@ -17,6 +17,7 @@ import bpy import inspect from .base import PlasmaModifierProperties +from .anim import * from .avatar import * from .logic import * from .physics import * diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py new file mode 100644 index 0000000..8bde3fc --- /dev/null +++ b/korman/properties/modifiers/anim.py @@ -0,0 +1,172 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from bpy.props import * +from PyHSPlasma import * + +from .base import PlasmaModifierProperties +from ...exporter import ExportError, utils + +def _convert_frame_time(frame_num): + fps = bpy.context.scene.render.fps + return frame_num / fps + +def _get_blender_action(bo): + if bo.animation_data is None or bo.animation_data.action is None: + raise ExportError("Object '{}' has no Action to export".format(bo.name)) + return bo.animation_data.action + +class PlasmaAnimationModifier(PlasmaModifierProperties): + pl_id = "animation" + + bl_category = "Animation" + bl_label = "Animation" + bl_description = "Object animation" + bl_icon = "ACTION" + + auto_start = BoolProperty(name="Auto Start", + description="Automatically start this animation on link-in", + default=True) + loop = BoolProperty(name="Loop Anim", + description="Loop the animation", + default=True) + + initial_marker = StringProperty(name="Start Marker", + description="Marker indicating the default start point") + loop_start = StringProperty(name="Loop Start", + description="Marker indicating where the default loop begins") + loop_end = StringProperty(name="Loop End", + description="Marker indicating where the default loop ends") + + def created(self, obj): + self.display_name = "{}_(Entire Animation)".format(obj.name) + + @property + def requires_actor(self): + return True + + def export(self, exporter, bo, so): + action = _get_blender_action(bo) + markers = action.pose_markers + + atcanim = exporter.mgr.find_create_key(plATCAnim, so=so, name=self.display_name).object + atcanim.autoStart = self.auto_start + atcanim.loop = self.loop + atcanim.name = "(Entire Animation)" + atcanim.start = 0.0 + atcanim.end = _convert_frame_time(action.frame_range[1]) + + # Simple start and loop info + initial_marker = markers.get(self.initial_marker) + if initial_marker is not None: + atcanim.initial = _convert_fame_time(initial_marker.frame) + else: + atcanim.initial = -1.0 + if self.loop: + loop_start = markers.get(self.loop_start) + if loop_start is not None: + atcanim.loopStart = _convert_frame_time(loop_start.frame) + else: + atcanim.loopStart = 0.0 + loop_end = markers.get(self.loop_end) + if loop_end is not None: + atcanim.loopEnd = _convert_frame_time(loop_end.frame) + else: + atcanim.loopEnd = _convert_frame_time(action.frame_range[1]) + + # Marker points + for marker in markers: + atcanim.setMarker(marker.name, _convert_frame_time(marker.frame)) + + # Fixme? Not sure if we really need to expose this... + atcanim.easeInMin = 1.0 + atcanim.easeInMax = 1.0 + atcanim.easeInLength = 1.0 + atcanim.easeOutMin = 1.0 + atcanim.easeOutMax = 1.0 + atcanim.easeOutLength = 1.0 + + # Now for the animation data. We're mostly just going to hand this off to the controller code + matrix = bo.matrix_basis + applicator = plMatrixChannelApplicator() + applicator.enabled = True + applicator.channelName = bo.name + channel = plMatrixControllerChannel() + channel.controller = exporter.animation.convert_action2tm(action, matrix) + applicator.channel = channel + atcanim.addApplicator(applicator) + + # Decompose the matrix into the 90s-era 3ds max affine parts sillyness + # All that's missing now is something like "(c) 1998 HeadSpin" oh wait... + affine = hsAffineParts() + affine.T = utils.vector3(matrix.to_translation()) + affine.Q = utils.quaternion(matrix.to_quaternion()) + affine.K = utils.vector3(matrix.to_scale()) + affine.F = -1.0 if matrix.determinant() < 0.0 else 1.0 + channel.affine = affine + + # We need both an AGModifier and an AGMasterMod + # TODO: grouped animations (eg one door, two objects) + agmod = exporter.mgr.add_object(plAGModifier, so=so, name=self.display_name) + agmod.channelName = bo.name + agmaster = exporter.mgr.add_object(plAGMasterMod, so=so, name=self.display_name) + agmaster.addPrivateAnim(atcanim.key) + + +class LoopMarker(bpy.types.PropertyGroup): + loop_name = StringProperty(name="Loop Name", + description="Name of this loop") + loop_start = StringProperty(name="Loop Start", + description="Marker name from whence the loop begins") + loop_end = StringProperty(name="Loop End", + description="Marker name from whence the loop ends") + + +class PlasmaAnimationLoopModifier(PlasmaModifierProperties): + pl_id = "animation_loop" + + bl_category = "Animation" + bl_label = "Loop Markers" + bl_description = "Animation loop settings" + bl_icon = "PMARKER_SEL" + + loops = CollectionProperty(name="Loops", + description="Loop points within the animation", + type=LoopMarker) + active_loop_index = IntProperty(options={"HIDDEN"}) + + def created(self, obj): + # who cares, this modifier creates no Keys... + self.display_name = "AnimLoops" + + def export(self, exporter, bo, so): + action = _get_blender_action(bo) + markers = action.pose_markers + + key_name = bo.plasma_modifiers.animation.display_name + atcanim = exporter.mgr.find_create_key(plATCAnim, so=so, name=key_name).object + for loop in self.loops: + start = markers.get(loop.loop_start) + end = markers.get(loop.loop_end) + if start is None: + exporter.report.warn("Animation '{}' Loop '{}': Marker '{}' not found. This loop will not be exported".format( + action.name, loop.loop_name, loop.loop_start), indent=2) + if end is None: + exporter.report.warn("Animation '{}' Loop '{}': Marker '{}' not found. This loop will not be exported".format( + action.name, loop.loop_name, loop.loop_end), indent=2) + if start is None or end is None: + continue + atcanim.setLoop(loop.loop_name, _convert_frame_time(start.frame), _convert_frame_time(end.frame)) diff --git a/korman/ui/modifiers/__init__.py b/korman/ui/modifiers/__init__.py index 7dcf614..0a838df 100644 --- a/korman/ui/modifiers/__init__.py +++ b/korman/ui/modifiers/__init__.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from .anim import * from .avatar import * from .logic import * from .physics import * diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py new file mode 100644 index 0000000..198eed4 --- /dev/null +++ b/korman/ui/modifiers/anim.py @@ -0,0 +1,70 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy + +def _check_for_anim(layout, context): + if context.object.animation_data is None or context.object.animation_data.action is None: + layout.label("Object has no animation data", icon="ERROR") + return False + return True + +def animation(modifier, layout, context): + if not _check_for_anim(layout, context): + return + action = context.object.animation_data.action + + split = layout.split() + col = split.column() + col.prop(modifier, "auto_start") + col = split.column() + col.prop(modifier, "loop") + + layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER") + col = layout.column() + col.enabled = modifier.loop + col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER") + col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER") + + +class LoopListUI(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): + layout.prop(item, "loop_name", emboss=False, text="", icon="PMARKER_ACT") + + +def animation_loop(modifier, layout, context): + if not _check_for_anim(layout, context): + return + + row = layout.row() + row.template_list("LoopListUI", "loops", modifier, "loops", modifier, "active_loop_index", + rows=2, maxrows=3) + col = row.column(align=True) + op = col.operator("object.plasma_modifier_collection_add", icon="ZOOMIN", text="") + op.modifier = modifier.pl_id + op.collection = "loops" + op.name_prefix = "Loop" + op.name_prop = "loop_name" + op = col.operator("object.plasma_modifier_collection_remove", icon="ZOOMOUT", text="") + op.modifier = modifier.pl_id + op.collection = "loops" + op.index = modifier.active_loop_index + + # Modify the loop points + if modifier.loops: + action = context.object.animation_data.action + loop = modifier.loops[modifier.active_loop_index] + layout.prop_search(loop, "loop_start", action, "pose_markers", icon="PMARKER") + layout.prop_search(loop, "loop_end", action, "pose_markers", icon="PMARKER") From 0ceca75f0daa8f503a678f2286f074c2ddc6b3c7 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 5 Jul 2015 01:34:18 -0400 Subject: [PATCH 2/3] Be smarter about animation subcontrols Plasma assumes that any non-root level plCompoundController cannot have any null subcontrollers. So, we now attempt to export leaf controllers where possible, instead of compounds. This is hindered by the fact that Blender loves bezier interpolation. --- korman/exporter/animation.py | 213 +++++++++++++++++++++++++---------- 1 file changed, 155 insertions(+), 58 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index c14102f..5aae46f 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -15,26 +15,17 @@ import bpy import math +import mathutils from PyHSPlasma import * import weakref +from . import utils + class AnimationConverter: def __init__(self, exporter): self._exporter = weakref.ref(exporter) self._bl_fps = bpy.context.scene.render.fps - def _check_scalar_subcontrollers(self, ctrl, default_xform): - """Ensures that all scalar subcontrollers have at least one keyframe in the default state""" - for i in ("X", "Y", "Z"): - sub = getattr(ctrl, i) - if not sub.hasKeys(): - keyframe = hsScalarKey() - keyframe.frame = 0 - keyframe.frameTime = 0.0 - keyframe.type = hsKeyFrame.kScalarKeyFrame - keyframe.value = getattr(default_xform, i.lower()) - sub.keys = ([keyframe,], hsKeyFrame.kScalarKeyFrame) - def convert_action2tm(self, action, default_xform): """Converts a Blender Action to a plCompoundController.""" fcurves = action.fcurves @@ -49,70 +40,176 @@ class AnimationConverter: tm.Z = self.make_scale_controller(fcurves, default_xform) return tm - def _is_bezier_curve(self, keyframes): - for i in keyframes: - if i.interpolation == "BEZIER": - return True - return False - def make_pos_controller(self, fcurves, default_xform): pos_curves = (i for i in fcurves if i.data_path == "location" and i.keyframe_points) - ctrl = self.make_scalar_controller(pos_curves) - if ctrl is not None and default_xform is not None: - self._check_scalar_subcontrollers(ctrl, default_xform.to_transpose()) + keyframes, bez_chans = self._process_keyframes(pos_curves) + if not keyframes: + return None + + # At one point, I had some... insanity here to try to crush bezier channels and hand off to + # blah blah blah... As it turns out, point3 keyframe's tangents are vector3s :) + ctrl = self._make_point3_controller(keyframes, bez_chans, default_xform.to_translation()) + return ctrl def make_rot_controller(self, fcurves, default_xform): + # TODO: support rotation_quaternion rot_curves = (i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points) - ctrl = self.make_scalar_controller(rot_curves) - if ctrl is not None and default_xform is not None: - self._check_scalar_subcontrollers(ctrl, default_xform.to_euler("XYZ")) + keyframes, bez_chans = self._process_keyframes(rot_curves) + if not keyframes: + return None + + # Ugh. Unfortunately, it appears Blender's default interpolation is bezier. So who knows if + # many users will actually see the benefit here? Makes me sad. + if bez_chans: + ctrl = self._make_scalar_controller(keyframes, bez_chans, default_xform.to_euler("XYZ")) + else: + ctrl = self._make_quat_controller(keyframes, default_xform.to_euler("XYZ")) return ctrl + def make_scale_controller(self, fcurves, default_xform): # ... TODO ... # who needs this anyway? return None - def make_scalar_controller(self, fcurves): - ctrl = plCompoundController() - subctls = ("X", "Y", "Z") + def _make_point3_controller(self, keyframes, bezier, default_xform): + ctrl = plLeafController() + subctrls = ("X", "Y", "Z") + keyframe_type = hsKeyFrame.kBezPoint3KeyFrame if bezier else hsKeyFrame.kPoint3KeyFrame + exported_frames = [] + last_xform = [default_xform[0], default_xform[1], default_xform[2]] + + for keyframe in keyframes: + exported = hsPoint3Key() + exported.frame = keyframe.frame_num + exported.frameTime = keyframe.frame_time + exported.type = keyframe_type + + in_tan = hsVector3() + out_tan = hsVector3() + value = hsVector3() + for i, subctrl in enumerate(subctrls): + fkey = keyframe.values.get(i, None) + if fkey is not None: + v = fkey.co[1] + last_xform[i] = v + setattr(value, subctrl, v) + setattr(in_tan, subctrl, keyframe.in_tans[i]) + setattr(out_tan, subctrl, keyframe.out_tans[i]) + else: + setattr(value, subctrl, last_xform[i]) + setattr(in_tan, subctrl, 0.0) + setattr(out_tan, subctrl, 0.0) + exported.inTan = in_tan + exported.outTan = out_tan + exported.value = value + exported_frames.append(exported) + ctrl.keys = (exported_frames, keyframe_type) + return ctrl - # this ensures that all subcontrollers are populated -- otherwise KABLOOEY! - for i in subctls: + def _make_quat_controller(self, keyframes, default_xform): + ctrl = plLeafController() + keyframe_type = hsKeyFrame.kQuatKeyFrame + exported_frames = [] + last_xform = [default_xform[0], default_xform[1], default_xform[2]] + + for keyframe in keyframes: + exported = hsQuatKey() + exported.frame = keyframe.frame_num + exported.frameTime = keyframe.frame_time + exported.type = keyframe_type + # NOTE: quat keyframes don't do bezier nonsense + + value = mathutils.Euler(last_xform, default_xform.order) + for i in range(3): + fkey = keyframe.values.get(i, None) + if fkey is not None: + v = fkey.co[1] + last_xform[i] = v + value[i] = v + quat = value.to_quaternion() + exported.value = utils.quaternion(quat) + exported_frames.append(exported) + ctrl.keys = (exported_frames, keyframe_type) + return ctrl + + def _make_scalar_controller(self, keyframes, bez_chans, default_xform): + ctrl = plCompoundController() + subctrls = ("X", "Y", "Z") + for i in subctrls: setattr(ctrl, i, plLeafController()) + exported_frames = ([], [], []) + + for keyframe in keyframes: + for i, subctrl in enumerate(subctrls): + fkey = keyframe.values.get(i, None) + if fkey is not None: + keyframe_type = hsKeyFrame.kBezScalarKeyFrame if i in bez_chans else hsKeyFrame.kScalarKeyFrame + exported = hsScalarKey() + exported.frame = keyframe.frame_num + exported.frameTime = keyframe.frame_time + exported.inTan = keyframe.in_tans[i] + exported.outTan = keyframe.out_tans[i] + exported.type = keyframe_type + exported.value = fkey.co[1] + exported_frames[i].append(exported) + for i, subctrl in enumerate(subctrls): + my_keyframes = exported_frames[i] + + # ensure this controller has at least ONE keyframe + if not my_keyframes: + hack_frame = hsScalarKey() + hack_frame.frame = 0 + hack_frame.frameTime = 0.0 + hack_frame.type = hsKeyFrame.kScalarKeyFrame + hack_frame.value = default_xform[i] + my_keyframes.append(hack_frame) + getattr(ctrl, subctrl).keys = (my_keyframes, my_keyframes[0].type) + return ctrl + + def _process_keyframes(self, fcurves): + """Groups all FCurves for the same frame together""" + keyframe_data = type("KeyFrameData", (), {}) + fps = self._bl_fps + pi = math.pi + keyframes = {} + bez_chans = set() for fcurve in fcurves: fcurve.update() - if self._is_bezier_curve(fcurve.keyframe_points): - key_type = hsKeyFrame.kScalarKeyFrame - else: - key_type = hsKeyFrame.kBezScalarKeyFrame - frames = [] - pi = math.pi - fps = self._bl_fps - - for i in fcurve.keyframe_points: - bl_frame_num, value = i.co - frame = hsScalarKey() - if i.interpolation == "BEZIER": - frame.inTan = -(value - i.handle_left[1]) / (bl_frame_num - i.handle_left[0]) / fps / (2 * pi) - frame.outTan = (value - i.handle_right[1]) / (bl_frame_num - i.handle_right[0]) / fps / (2 * pi) + for fkey in fcurve.keyframe_points: + frame_num, value = fkey.co + if fps == 30.0: + # hope you don't have a frame 29.9 and frame 30.0... + frame_num = int(frame_num) else: - frame.inTan = 0.0 - frame.outTan = 0.0 - frame.type = key_type - frame.frame = int(bl_frame_num * (30.0 / fps)) - frame.frameTime = bl_frame_num / fps - frame.value = value - frames.append(frame) - controller = plLeafController() - getattr(ctrl, subctls[fcurve.array_index]).keys = (frames, key_type) - - # Compact this bamf - if not ctrl.X.hasKeys() and not ctrl.Y.hasKeys() and not ctrl.Z.hasKeys(): - return None - else: - return ctrl + frame_num = int(frame_num * (30.0 / fps)) + keyframe = keyframes.get(frame_num, None) + if keyframe is None: + keyframe = keyframe_data() + keyframe.frame_num = frame_num + keyframe.frame_time = frame_num / fps + keyframe.in_tans = {} + keyframe.out_tans = {} + keyframe.values = {} + keyframes[frame_num] = keyframe + idx = fcurve.array_index + keyframe.values[idx] = fkey + + # Calculate the bezier interpolation nonsense + if fkey.interpolation == "BEZIER": + og_frame = fkey.co[0] + keyframe.in_tans[idx] = -(value - fkey.handle_left[1]) / (og_frame - fkey.handle_left[0]) / fps / (2 * pi) + keyframe.out_tans[idx] = (value - fkey.handle_right[1]) / (og_frame - fkey.handle_right[0]) / fps / (2 * pi) + else: + keyframe.in_tans[idx] = 0.0 + keyframe.out_tans[idx] = 0.0 + if keyframe.in_tans[idx] != 0.0 or keyframe.out_tans[idx] != 0.0: + bez_chans.add(idx) + + # Return the keyframes in a sequence sorted by frame number + final_keyframes = [keyframes[i] for i in sorted(keyframes)] + return (final_keyframes, bez_chans) @property def _mgr(self): From 4a8e0722f96d967fd1f194a45be1026a1ff5c19b Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 5 Jul 2015 21:02:18 -0400 Subject: [PATCH 3/3] Implement scale animation controllers --- korman/exporter/animation.py | 53 ++++++++++++++++++++++++++--- korman/properties/modifiers/anim.py | 5 ++- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 5aae46f..bdf3781 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -66,11 +66,15 @@ class AnimationConverter: ctrl = self._make_quat_controller(keyframes, default_xform.to_euler("XYZ")) return ctrl - def make_scale_controller(self, fcurves, default_xform): - # ... TODO ... - # who needs this anyway? - return None + scale_curves = (i for i in fcurves if i.data_path == "scale" and i.keyframe_points) + keyframes, bez_chans = self._process_keyframes(scale_curves) + if not keyframes: + return None + + # There is no such thing as a compound scale controller... in Plasma, anyway. + ctrl = self._make_scale_value_controller(keyframes, bez_chans, default_xform) + return ctrl def _make_point3_controller(self, keyframes, bezier, default_xform): ctrl = plLeafController() @@ -167,6 +171,47 @@ class AnimationConverter: getattr(ctrl, subctrl).keys = (my_keyframes, my_keyframes[0].type) return ctrl + def _make_scale_value_controller(self, keyframes, bez_chans, default_xform): + subctrls = ("X", "Y", "Z") + keyframe_type = hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame + exported_frames = [] + + _scale = default_xform.to_scale() + last_xform = [_scale[0], _scale[1], _scale[2]] + unit_quat = default_xform.to_quaternion() + unit_quat.normalize() + unit_quat = utils.quaternion(unit_quat) + + for keyframe in keyframes: + exported = hsScaleKey() + exported.frame = keyframe.frame_num + exported.frameTime = keyframe.frame_time + exported.type = keyframe_type + + in_tan = hsVector3() + out_tan = hsVector3() + value = hsVector3() + for i, subctrl in enumerate(subctrls): + fkey = keyframe.values.get(i, None) + if fkey is not None: + v = fkey.co[1] + last_xform[i] = v + setattr(value, subctrl, v) + setattr(in_tan, subctrl, keyframe.in_tans[i]) + setattr(out_tan, subctrl, keyframe.out_tans[i]) + else: + setattr(value, subctrl, last_xform[i]) + setattr(in_tan, subctrl, 0.0) + setattr(out_tan, subctrl, 0.0) + exported.inTan = in_tan + exported.outTan = out_tan + exported.value = (value, unit_quat) + exported_frames.append(exported) + + ctrl = plLeafController() + ctrl.keys = (exported_frames, keyframe_type) + return ctrl + def _process_keyframes(self, fcurves): """Groups all FCurves for the same frame together""" keyframe_data = type("KeyFrameData", (), {}) diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 8bde3fc..8475085 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -113,9 +113,12 @@ class PlasmaAnimationModifier(PlasmaModifierProperties): # All that's missing now is something like "(c) 1998 HeadSpin" oh wait... affine = hsAffineParts() affine.T = utils.vector3(matrix.to_translation()) - affine.Q = utils.quaternion(matrix.to_quaternion()) affine.K = utils.vector3(matrix.to_scale()) affine.F = -1.0 if matrix.determinant() < 0.0 else 1.0 + rot = matrix.to_quaternion() + affine.Q = utils.quaternion(rot) + rot.normalize() + affine.U = utils.quaternion(rot) channel.affine = affine # We need both an AGModifier and an AGMasterMod