diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 09c2663..f800f55 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -14,10 +14,13 @@ # along with Korman. If not, see . import bpy +from collections import defaultdict +import functools import itertools import math import mathutils from PyHSPlasma import * +from typing import * import weakref from . import utils @@ -27,10 +30,10 @@ class AnimationConverter: self._exporter = weakref.ref(exporter) self._bl_fps = bpy.context.scene.render.fps - def _convert_frame_time(self, frame_num): + def _convert_frame_time(self, frame_num : int) -> float: return frame_num / self._bl_fps - def convert_object_animations(self, bo, so): + def convert_object_animations(self, bo, so) -> None: if not bo.plasma_object.has_animation_data: return @@ -191,7 +194,7 @@ class AnimationConverter: return None energy_curve = next((i for i in fcurves if i.data_path == "energy" and i.keyframe_points), None) - color_curves = sorted((i for i in fcurves if i.data_path == "color" and i.keyframe_points), key=lambda x: x.array_index) + color_curves = [i for i in fcurves if i.data_path == "color" and i.keyframe_points] if energy_curve is None and color_curves is None: return None elif lamp.use_only_shadow: @@ -202,10 +205,15 @@ class AnimationConverter: return None # OK Specular is easy. We just toss out the color as a point3. - color_keyframes, color_bez = self._process_keyframes(color_curves, convert=lambda x: x * -1.0 if lamp.use_negative else None) + def convert_specular_animation(color): + if lamp.use_negative: + return map(lambda x: x * -1.0, color) + else: + return color + color_keyframes, color_bez = self._process_keyframes(color_curves, 3, lamp.color, convert_specular_animation) if color_keyframes and lamp.use_specular: channel = plPointControllerChannel() - channel.controller = self._make_point3_controller(color_curves, color_keyframes, color_bez, lamp.color) + channel.controller = self._make_point3_controller(color_keyframes, color_bez) applicator = plLightSpecularApplicator() applicator.channelName = name applicator.channel = channel @@ -214,18 +222,20 @@ class AnimationConverter: # Hey, look, it's a third way to process FCurves. YAY! def convert_diffuse_animation(color, energy): if lamp.use_negative: - return { key: (0.0 - value) * energy[0] for key, value in color.items() } + proc = lambda x: x * -1.0 * energy[0] else: - return { key: value * energy[0] for key, value in color.items() } - diffuse_defaults = { "color": lamp.color, "energy": lamp.energy } + proc = lambda x: x * energy[0] + return map(proc, color) + diffuse_channels = dict(color=3, energy=1) + diffuse_defaults = dict(color=lamp.color, energy=lamp.energy) diffuse_fcurves = color_curves + [energy_curve,] - diffuse_keyframes = self._process_fcurves(diffuse_fcurves, convert_diffuse_animation, diffuse_defaults) + diffuse_keyframes = self._process_fcurves(diffuse_fcurves, diffuse_channels, 3, convert_diffuse_animation, diffuse_defaults) if not diffuse_keyframes: return None # Whew. channel = plPointControllerChannel() - channel.controller = self._make_point3_controller([], diffuse_keyframes, False, []) + channel.controller = self._make_point3_controller(diffuse_keyframes, False) applicator = plLightDiffuseApplicator() applicator.channelName = name applicator.channel = channel @@ -239,8 +249,16 @@ class AnimationConverter: distance_fcurve = next((i for i in fcurves if i.data_path == "distance"), None) if energy_fcurve is None and distance_fcurve is None: return None - light_converter = self._exporter().light - intensity, atten_end = light_converter.convert_attenuation(lamp) + + light_converter, report = self._exporter().light, self._exporter().report + omni_fcurves = [distance_fcurve, energy_fcurve] + omni_channels = dict(distance=1, energy=1) + omni_defaults = dict(distance=lamp.distance, energy=lamp.energy) + + def convert_omni_atten(distance, energy): + intens = abs(energy[0]) + atten_end = distance[0] if lamp.use_sphere else distance[0] * 2 + return light_converter.convert_attenuation_linear(intens, atten_end) # All types allow animating cutoff if distance_fcurve is not None: @@ -255,15 +273,9 @@ class AnimationConverter: falloff = lamp.falloff_type if falloff == "CONSTANT": if energy_fcurve is not None: - self._exporter().report.warn("Constant attenuation cannot be animated in Plasma", ident=3) + report.warn("Constant attenuation cannot be animated in Plasma", ident=3) elif falloff == "INVERSE_LINEAR": - def convert_linear_atten(distance, energy): - intens = abs(energy[0]) - atten_end = distance[0] if lamp.use_sphere else distance[0] * 2 - return light_converter.convert_attenuation_linear(intens, atten_end) - - keyframes = self._process_fcurves([distance_fcurve, energy_fcurve], convert_linear_atten, - {"distance": lamp.distance, "energy": lamp.energy}) + keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, omni_defaults) if keyframes: channel = plScalarControllerChannel() channel.controller = self._make_scalar_leaf_controller(keyframes, False) @@ -273,13 +285,8 @@ class AnimationConverter: yield applicator elif falloff == "INVERSE_SQUARE": if self._mgr.getVer() >= pvMoul: - def convert_quadratic_atten(distance, energy): - intens = abs(energy[0]) - atten_end = distance[0] if lamp.use_sphere else distance[0] * 2 - return light_converter.convert_attenuation_quadratic(intens, atten_end) - - keyframes = self._process_fcurves([distance_fcurve, energy_fcurve], convert_quadratic_atten, - {"distance": lamp.distance, "energy": lamp.energy}) + report.port("Lamp {} Falloff animations are only supported in Myst Online: Uru Live", falloff, indent=3) + keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, omni_defaults) if keyframes: channel = plScalarControllerChannel() channel.controller = self._make_scalar_leaf_controller(keyframes, False) @@ -288,9 +295,9 @@ class AnimationConverter: applicator.channel = channel yield applicator else: - self._exporter().report.port("Lamp Falloff '{}' animations only partially supported for this version of Plasma", falloff, indent=3) + report.warn("Lamp {} Falloff animations are not supported for this version of Plasma", falloff, indent=3) else: - self._exporter().report.warn("Lamp Falloff '{}' animations are not supported".format(falloff), ident=3) + report.warn("Lamp Falloff '{}' animations are not supported", falloff, ident=3) def _convert_sound_volume_animation(self, name, fcurves, soundemit): if not fcurves: @@ -340,8 +347,11 @@ class AnimationConverter: size = spot_size[0] value = size - (blend * size) return math.degrees(value) - defaults = { "spot_blend": lamp.spot_blend, "spot_size": lamp.spot_size } - keyframes = self._process_fcurves([blend_fcurve, size_fcurve], convert_spot_inner, defaults) + + inner_fcurves = [blend_fcurve, size_fcurve] + inner_channels = dict(spot_blend=1, spot_size=1) + inner_defaults = dict(spot_blend=lamp.spot_blend, spot_size=lamp.spot_size) + keyframes = self._process_fcurves(inner_fcurves, inner_channels, 1, convert_spot_inner, inner_defaults) if keyframes: channel = plScalarControllerChannel() @@ -351,7 +361,7 @@ class AnimationConverter: applicator.channel = channel yield applicator - def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False): + def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False) -> Union[None, plMatrixChannelApplicator]: tm = self.convert_transform_controller(fcurves, xform, allow_empty) if tm is None and not allow_empty: return None @@ -366,13 +376,14 @@ class AnimationConverter: return applicator - def convert_transform_controller(self, fcurves, xform, allow_empty=False): + def convert_transform_controller(self, fcurves, xform, allow_empty=False) -> Union[None, plCompoundController]: if not fcurves and not allow_empty: return None - pos = self.make_pos_controller(fcurves, xform) - rot = self.make_rot_controller(fcurves, xform) - scale = self.make_scale_controller(fcurves, xform) + pos = self.make_pos_controller(fcurves, "location", xform.to_translation()) + # TODO: support rotation_quaternion + rot = self.make_rot_controller(fcurves, "rotation_euler", xform.to_euler()) + scale = self.make_scale_controller(fcurves, "scale", xform.to_scale()) if pos is None and rot is None and scale is None: if not allow_empty: return None @@ -383,17 +394,17 @@ class AnimationConverter: tm.Z = scale return tm - def get_anigraph_keys(self, bo=None, so=None): + def get_anigraph_keys(self, bo=None, so=None) -> Tuple[plKey, plKey]: mod = self._mgr.find_create_key(plAGModifier, so=so, bl=bo) master = self._mgr.find_create_key(plAGMasterMod, so=so, bl=bo) return mod, master - def get_anigraph_objects(self, bo=None, so=None): + def get_anigraph_objects(self, bo=None, so=None) -> Tuple[plAGModifier, plAGMasterMod]: mod = self._mgr.find_create_object(plAGModifier, so=so, bl=bo) master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo) return mod, master - def get_animation_key(self, bo, so=None): + def get_animation_key(self, bo, so=None) -> plKey: # we might be controlling more than one animation. isn't that cute? # https://www.youtube.com/watch?v=hspNaoxzNbs # (but obviously this is not wrong...) @@ -403,72 +414,65 @@ class AnimationConverter: else: return self.get_anigraph_keys(bo, so)[1] - def make_matrix44_controller(self, fcurves, pos_path, scale_path, pos_default, scale_default): - def convert_matrix_keyframe(**kwargs): - pos = kwargs.get(pos_path) - scale = kwargs.get(scale_path) - - # Since only some position curves may be supplied, construct dict with all positions - allpos = dict(enumerate(pos_default)) - allscale = dict(enumerate(scale_default)) - allpos.update(pos) - allscale.update(scale) + def make_matrix44_controller(self, fcurves, pos_path : str, scale_path : str, pos_default, scale_default) -> Union[None, plLeafController]: + def convert_matrix_keyframe(**kwargs) -> hsMatrix44: + pos = kwargs[pos_path] + scale = kwargs[scale_path] matrix = hsMatrix44() - # Note: scale and pos are dicts, so we can't unpack - matrix.setTranslate(hsVector3(allpos[0], allpos[1], allpos[2])) - matrix.setScale(hsVector3(allscale[0], allscale[1], allscale[2])) + matrix.setTranslate(hsVector3(*pos)) + matrix.setScale(hsVector3(*scale)) return matrix fcurves = [i for i in fcurves if i.data_path == pos_path or i.data_path == scale_path] if not fcurves: return None + channels = { pos_path: 3, scale_path: 3 } default_values = { pos_path: pos_default, scale_path: scale_default } - keyframes = self._process_fcurves(fcurves, convert_matrix_keyframe, default_values) + keyframes = self._process_fcurves(fcurves, channels, 1, convert_matrix_keyframe, default_values) if not keyframes: return None # Now we make the controller return self._make_matrix44_controller(keyframes) - def make_pos_controller(self, fcurves, default_xform, convert=None): - pos_curves = [i for i in fcurves if i.data_path == "location" and i.keyframe_points] - keyframes, bez_chans = self._process_keyframes(pos_curves, convert) + def make_pos_controller(self, fcurves, data_path : str, default_xform, convert=None) -> Union[None, plLeafController]: + pos_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] + keyframes, bez_chans = self._process_keyframes(pos_curves, 3, default_xform, convert) 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(pos_curves, keyframes, bez_chans, default_xform.to_translation()) + ctrl = self._make_point3_controller(keyframes, bez_chans) return ctrl - def make_rot_controller(self, fcurves, default_xform, convert=None): - # TODO: support rotation_quaternion - rot_curves = [i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points] - keyframes, bez_chans = self._process_keyframes(rot_curves, convert=None) + def make_rot_controller(self, fcurves, data_path : str, default_xform, convert=None) -> Union[None, plCompoundController, plLeafController]: + rot_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] + keyframes, bez_chans = self._process_keyframes(rot_curves, 3, default_xform, convert=None) 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_compound_controller(rot_curves, keyframes, bez_chans, default_xform.to_euler()) + ctrl = self._make_scalar_compound_controller(keyframes, bez_chans) else: - ctrl = self._make_quat_controller(rot_curves, keyframes, default_xform.to_euler()) + ctrl = self._make_quat_controller( keyframes) return ctrl - def make_scale_controller(self, fcurves, default_xform, convert=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, convert) + def make_scale_controller(self, fcurves, data_path : str, default_xform, convert=None) -> plLeafController: + scale_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] + keyframes, bez_chans = self._process_keyframes(scale_curves, 3, default_xform, convert) if not keyframes: return None # There is no such thing as a compound scale controller... in Plasma, anyway. - ctrl = self._make_scale_value_controller(scale_curves, keyframes, bez_chans, default_xform) + ctrl = self._make_scale_value_controller(keyframes, bez_chans) return ctrl - def make_scalar_leaf_controller(self, fcurve, convert=None): + def make_scalar_leaf_controller(self, fcurve, convert=None) -> Union[None, plLeafController]: keyframes, bezier = self._process_fcurve(fcurve, convert) if not keyframes: return None @@ -476,7 +480,7 @@ class AnimationConverter: ctrl = self._make_scalar_leaf_controller(keyframes, bezier) return ctrl - def _make_matrix44_controller(self, keyframes): + def _make_matrix44_controller(self, keyframes) -> plLeafController: ctrl = plLeafController() keyframe_type = hsKeyFrame.kMatrix44KeyFrame exported_frames = [] @@ -486,52 +490,32 @@ class AnimationConverter: exported.frame = keyframe.frame_num exported.frameTime = keyframe.frame_time exported.type = keyframe_type - exported.value = keyframe.value + exported.value = keyframe.values[0] exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_point3_controller(self, fcurves, keyframes, bezier, default_xform): + def _make_point3_controller(self, keyframes, bezier) -> plLeafController: ctrl = plLeafController() - subctrls = ("X", "Y", "Z") keyframe_type = hsKeyFrame.kBezPoint3KeyFrame if bezier else hsKeyFrame.kPoint3KeyFrame exported_frames = [] - ctrl_fcurves = { i.array_index: i for i in fcurves } 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): - fval = keyframe.values.get(i, None) - if fval is not None: - setattr(value, subctrl, fval) - setattr(in_tan, subctrl, keyframe.in_tans[i]) - setattr(out_tan, subctrl, keyframe.out_tans[i]) - else: - try: - setattr(value, subctrl, ctrl_fcurves[i].evaluate(keyframe.frame_num_blender)) - except KeyError: - setattr(value, subctrl, default_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.inTan = hsVector3(*keyframe.in_tans) + exported.outTan = hsVector3(*keyframe.out_tans) + exported.value = hsVector3(*keyframe.values) exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_quat_controller(self, fcurves, keyframes, default_xform): + def _make_quat_controller(self, keyframes) -> plLeafController: ctrl = plLeafController() keyframe_type = hsKeyFrame.kQuatKeyFrame exported_frames = [] - ctrl_fcurves = { i.array_index: i for i in fcurves } for keyframe in keyframes: exported = hsQuatKey() @@ -540,58 +524,36 @@ class AnimationConverter: exported.type = keyframe_type # NOTE: quat keyframes don't do bezier nonsense - value = mathutils.Euler() - for i in range(3): - fval = keyframe.values.get(i, None) - if fval is not None: - value[i] = fval - else: - try: - value[i] = ctrl_fcurves[i].evaluate(keyframe.frame_num_blender) - except KeyError: - value[i] = default_xform[i] - quat = value.to_quaternion() - exported.value = utils.quaternion(quat) + value = mathutils.Euler(keyframe.values) + exported.value = utils.quaternion(value.to_quaternion()) exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_scalar_compound_controller(self, fcurves, keyframes, bez_chans, default_xform): + def _make_scalar_compound_controller(self, keyframes, bez_chans) -> plCompoundController: ctrl = plCompoundController() subctrls = ("X", "Y", "Z") for i in subctrls: setattr(ctrl, i, plLeafController()) exported_frames = ([], [], []) - ctrl_fcurves = { i.array_index: i for i in fcurves } for keyframe in keyframes: for i, subctrl in enumerate(subctrls): - fval = keyframe.values.get(i, None) - if fval 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 = fval - exported_frames[i].append(exported) + 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 = keyframe.values[i] + 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 _make_scalar_leaf_controller(self, keyframes, bezier): + def _make_scalar_leaf_controller(self, keyframes, bezier) -> plLeafController: ctrl = plLeafController() keyframe_type = hsKeyFrame.kBezScalarKeyFrame if bezier else hsKeyFrame.kScalarKeyFrame exported_frames = [] @@ -600,239 +562,192 @@ class AnimationConverter: exported = hsScalarKey() exported.frame = keyframe.frame_num exported.frameTime = keyframe.frame_time - exported.inTan = keyframe.in_tan - exported.outTan = keyframe.out_tan + exported.inTan = keyframe.in_tans[0] + exported.outTan = keyframe.out_tans[0] exported.type = keyframe_type - exported.value = keyframe.value + exported.value = keyframe.values[0] exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_scale_value_controller(self, fcurves, keyframes, bez_chans, default_xform): - subctrls = ("X", "Y", "Z") + def _make_scale_value_controller(self, keyframes, bez_chans) -> plLeafController: keyframe_type = hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame exported_frames = [] - ctrl_fcurves = { i.array_index: i for i in fcurves } - default_scale = default_xform.to_scale() - unit_quat = default_xform.to_quaternion() - unit_quat.normalize() - unit_quat = utils.quaternion(unit_quat) + # Hmm... This smells... But it was basically doing this before the rewrite. + unit_quat = hsQuat(0.0, 0.0, 0.0, 1.0) 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): - fval = keyframe.values.get(i, None) - if fval is not None: - setattr(value, subctrl, fval) - setattr(in_tan, subctrl, keyframe.in_tans[i]) - setattr(out_tan, subctrl, keyframe.out_tans[i]) - else: - try: - setattr(value, subctrl, ctrl_fcurves[i].evaluate(keyframe.frame_num_blender)) - except KeyError: - setattr(value, subctrl, default_scale[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.inTan = hsVector3(*keyframe.in_tans) + exported.outTan = hsVector3(*keyframe.out_tans) + exported.value = (hsVector3(*keyframe.values), unit_quat) exported_frames.append(exported) ctrl = plLeafController() ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _process_fcurve(self, fcurve, convert=None): + def _sort_and_dedupe_keyframes(self, keyframes : Dict) -> Sequence: + """Takes in the final, unsorted keyframe sequence and sorts it. If all keyframes are + equivalent, eg due to a convert function, then they are discarded.""" + + num_keyframes = len(keyframes) + keyframes_sorted = [keyframes[i] for i in sorted(keyframes)] + + # If any keyframe's value is equivalent to its boundary keyframes, discard it. + def filter_boundaries(i): + if i == 0 or i == num_keyframes - 1: + return False + left, me, right = keyframes_sorted[i - 1], keyframes_sorted[i], keyframes_sorted[i + 1] + return left.values == me.values == right.values + + filtered_indices = list(itertools.filterfalse(filter_boundaries, range(num_keyframes))) + if len(filtered_indices) == 2: + if keyframes_sorted[filtered_indices[0]].values == keyframes_sorted[filtered_indices[1]].values: + return [] + return [keyframes_sorted[i] for i in filtered_indices] + + def _process_fcurve(self, fcurve, convert=None) -> Tuple[Sequence, AbstractSet]: """Like _process_keyframes, but for one fcurve""" + + # Adapt from incoming single item sequence to a single argument. + single_convert = lambda x: convert(x[0]) if convert is not None else None + # Can't proxy to _process_fcurves because it only supports linear interoplation. + return self._process_keyframes([fcurve], 1, [0.0], single_convert) + + def _santize_converted_values(self, num_channels : int, raw_values : Union[Dict, Sequence], convert : Callable): + assert convert is not None + if isinstance(raw_values, Dict): + values = convert(**raw_values) + elif isinstance(raw_values, Sequence): + values = convert(raw_values) + else: + raise AssertionError("Unexpected type for raw_values: {}".format(raw_values.__class__)) + + if not isinstance(values, Sequence) and isinstance(values, Iterable): + values = tuple(values) + if not isinstance(values, Sequence): + assert num_channels == 1, "Converter returned 1 value but expected {}".format(num_channels) + values = (values,) + else: + assert len(values) == num_channels, "Converter returned {} values but expected {}".format(len(values), num_channels) + return values + + def _process_fcurves(self, fcurves : Sequence, channels : Dict[str, int], result_channels : int, + convert : Callable, defaults : Dict[str, Union[float, Sequence]]) -> Sequence: + """This consumes a sequence of Blender FCurves that map to a single Plasma controller. + Like `_process_keyframes()`, except the converter function is mandatory, and each + Blender `data_path` must have a fixed number of channels. + """ + + # TODO: This fxn should probably issue a warning if any keyframes use bezier interpolation. + # But there's no indication given by any other fxn when an invalid interpolation mode is + # given, so what can you do? keyframe_data = type("KeyFrameData", (), {}) - fps = self._bl_fps - pi = math.pi + fps, pi = self._bl_fps, math.pi - keyframes = {} - bezier = False - fcurve.update() - for fkey in fcurve.keyframe_points: - keyframe = keyframe_data() - frame_num, value = fkey.co - if fps == 30.0: - keyframe.frame_num = int(frame_num) - else: - keyframe.frame_num = int(frame_num * (30.0 / fps)) - keyframe.frame_time = frame_num / fps - if fkey.interpolation == "BEZIER": - keyframe.in_tan = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi) - keyframe.out_tan = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi) - bezier = True - else: - keyframe.in_tan = 0.0 - keyframe.out_tan = 0.0 - keyframe.value = value if convert is None else convert(value) - keyframes[frame_num] = keyframe - final_keyframes = [keyframes[i] for i in sorted(keyframes)] - return (final_keyframes, bezier) - - def _process_fcurves(self, fcurves, convert, defaults=None): - """Processes FCurves of different data sets and converts them into a single list of keyframes. - This should be used when multiple Blender fields map to a single Plasma option.""" - class KeyFrameData: - def __init__(self): - self.values = {} - fps = self._bl_fps - pi = math.pi - - # It is assumed therefore that any multichannel FCurves will have all channels represented. - # This seems fairly safe with my experiments with Lamp colors... - grouped_fcurves = {} - for fcurve in fcurves: - if fcurve is None: - continue + grouped_fcurves = defaultdict(dict) + for fcurve in (i for i in fcurves if i is not None): fcurve.update() - if fcurve.data_path in grouped_fcurves: - grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve - else: - grouped_fcurves[fcurve.data_path] = { fcurve.array_index: fcurve } + grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve - # Default values for channels that are not animated - for key, value in defaults.items(): - if key not in grouped_fcurves: - if hasattr(value, "__len__"): - grouped_fcurves[key] = value + fcurve_keyframes = defaultdict(functools.partial(defaultdict, dict)) + for fcurve in (i for i in fcurves if i is not None): + for fkey in fcurve.keyframe_points: + fcurve_keyframes[fkey.co[0]][fcurve.data_path][fcurve.array_index] = fkey + + def iter_channel_values(frame_num : int, fcurves : Dict, fkeys : Dict, num_channels : int, defaults : Union[float, Sequence]): + for i in range(num_channels): + fkey = fkeys.get(i, None) + if fkey is None: + fcurve = fcurves.get(i, None) + if fcurve is None: + # We would like to test this to see if it makes sense, but Blender's mathutils + # types don't actually implement the sequence protocol. So, we'll have to + # just try to subscript it and see what happens. + try: + yield defaults[i] + except: + assert num_channels == 1, "Got a non-subscriptable default for a multi-channel keyframe." + yield defaults + else: + yield fcurve.evaluate(frame_num) else: - grouped_fcurves[key] = [value,] + yield fkey.co[1] - # Assemble a dict { PlasmaFrameNum: { FCurveDataPath: KeyFrame } } - keyframe_points = {} - for fcurve in fcurves: - if fcurve is None: - continue - for keyframe in fcurve.keyframe_points: - frame_num_blender, value = keyframe.co - frame_num = int(frame_num_blender * (30.0 / fps)) - - # This is a temporary keyframe, so we're not going to worry about converting everything - # Only the frame number to Plasma so we can go ahead and merge any rounded dupes - entry, data = keyframe_points.get(frame_num), None - if entry is None: - entry = {} - keyframe_points[frame_num] = entry - else: - data = entry.get(fcurve.data_path) - if data is None: - data = KeyFrameData() - data.frame_num = frame_num - data.frame_num_blender = frame_num_blender - entry[fcurve.data_path] = data - data.values[fcurve.array_index] = value - - # Now, we loop through our assembled keyframes and interpolate any missing data using the FCurves - fcurve_chans = { key: len(value) for key, value in grouped_fcurves.items() } - expected_values = sum(fcurve_chans.values()) - all_chans = frozenset(grouped_fcurves.keys()) - - # We will also do the final convert here as well... - final_keyframes = [] - - for frame_num in sorted(keyframe_points.copy().keys()): - keyframes = keyframe_points[frame_num] - frame_num_blender = next(iter(keyframes.values())).frame_num_blender - - # If any data_paths are missing, init a dummy - missing_channels = all_chans - frozenset(keyframes.keys()) - for chan in missing_channels: - dummy = KeyFrameData() - dummy.frame_num = frame_num - dummy.frame_num_blender = frame_num_blender - keyframes[chan] = dummy - - # Ensure all values are filled out. - num_values = sum(map(len, (i.values for i in keyframes.values()))) - if num_values != expected_values: - for chan, sorted_fcurves in grouped_fcurves.items(): - chan_keyframes = keyframes[chan] - chan_values = fcurve_chans[chan] - if len(chan_keyframes.values) == chan_values: - continue - for i in range(chan_values): - if i not in chan_keyframes.values: - try: - fcurve = grouped_fcurves[chan][i] - except: - chan_keyframes.values[i] = defaults[chan] - else: - if isinstance(fcurve, bpy.types.FCurve): - chan_keyframes.values[i] = fcurve.evaluate(chan_keyframes.frame_num_blender) - else: - # it's actually a default value! - chan_keyframes.values[i] = fcurve - - # All values are calculated! Now we convert the disparate key data into a single keyframe. - kwargs = { data_path: keyframe.values for data_path, keyframe in keyframes.items() } - final_keyframe = KeyFrameData() - final_keyframe.frame_num = frame_num - final_keyframe.frame_num_blender = frame_num_blender - final_keyframe.frame_time = frame_num / fps - value = convert(**kwargs) - if hasattr(value, "__len__"): - final_keyframe.in_tans = [0.0] * len(value) - final_keyframe.out_tans = [0.0] * len(value) - final_keyframe.values = value - else: - final_keyframe.in_tan = 0.0 - final_keyframe.out_tan = 0.0 - final_keyframe.value = value - final_keyframes.append(final_keyframe) - return final_keyframes + keyframes = {} + for frame_num, fkeys in fcurve_keyframes.items(): + keyframe = keyframe_data() + # hope you don't have a frame 29.9 and frame 30.0... + keyframe.frame_num = int(frame_num * (30.0 / fps)) + keyframe.frame_num_blender = frame_num + keyframe.frame_time = frame_num / fps + keyframe.values_raw = { data_path: tuple(iter_channel_values(frame_num, grouped_fcurves[data_path], fkeys, num_channels, defaults[data_path])) + for data_path, num_channels in channels.items() } + keyframe.values = self._santize_converted_values(result_channels, keyframe.values_raw, convert) + # Very gnawty + keyframe.in_tans = [0.0] * result_channels + keyframe.out_tans = [0.0] * result_channels + keyframes[frame_num] = keyframe - def _process_keyframes(self, fcurves, convert=None): + return self._sort_and_dedupe_keyframes(keyframes) + + def _process_keyframes(self, fcurves, num_channels : int, default_values : Sequence, convert=None) -> Tuple[Sequence, AbstractSet]: """Groups all FCurves for the same frame together""" keyframe_data = type("KeyFrameData", (), {}) - fps = self._bl_fps - pi = math.pi + fps, pi = self._bl_fps, math.pi - keyframes = {} - bez_chans = set() - for fcurve in fcurves: + keyframes, fcurve_keyframes = {}, defaultdict(dict) + + indexed_fcurves = { fcurve.array_index: fcurve for fcurve in fcurves if fcurve is not None } + for i, fcurve in indexed_fcurves.items(): fcurve.update() for fkey in fcurve.keyframe_points: - frame_num, value = fkey.co - keyframe = keyframes.get(frame_num, None) - if keyframe is None: - keyframe = keyframe_data() - if fps == 30.0: - # hope you don't have a frame 29.9 and frame 30.0... - keyframe.frame_num = int(frame_num) - else: - keyframe.frame_num = int(frame_num * (30.0 / fps)) - keyframe.frame_num_blender = 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] = value if convert is None else convert(value) - - # Calculate the bezier interpolation nonsense - if fkey.interpolation == "BEZIER": - keyframe.in_tans[idx] = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi) - keyframe.out_tans[idx] = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi) - bez_chans.add(idx) + fcurve_keyframes[fkey.co[0]][i] = fkey + + def iter_values(frame_num, fkeys) -> Generator[float, None, None]: + for i in range(num_channels): + fkey = fkeys.get(i, None) + if fkey is not None: + yield fkey.co[1] else: - keyframe.in_tans[idx] = 0.0 - keyframe.out_tans[idx] = 0.0 + fcurve = indexed_fcurves.get(i, None) + if fcurve is not None: + yield fcurve.evaluate(frame_num) + else: + yield default_values[i] + + # Does this really need to be a set? + bez_chans = set() + + for frame_num, fkeys in fcurve_keyframes.items(): + keyframe = keyframe_data() + # hope you don't have a frame 29.9 and frame 30.0... + keyframe.frame_num = int(frame_num * (30.0 / fps)) + keyframe.frame_num_blender = frame_num + keyframe.frame_time = frame_num / fps + keyframe.in_tans = [0.0] * num_channels + keyframe.out_tans = [0.0] * num_channels + keyframe.values_raw = tuple(iter_values(frame_num, fkeys)) + if convert is None: + keyframe.values = keyframe.values_raw + else: + keyframe.values = self._santize_converted_values(num_channels, keyframe.values_raw, convert) + + for i, fkey in ((i, fkey) for i, fkey in fkeys.items() if fkey.interpolation == "BEZIER"): + value = keyframe.values_raw[i] + keyframe.in_tans[i] = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi) + keyframe.out_tans[i] = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi) + bez_chans.add(i) + keyframes[frame_num] = keyframe # Return the keyframes in a sequence sorted by frame number - final_keyframes = [keyframes[i] for i in sorted(keyframes)] - return (final_keyframes, bez_chans) + return (self._sort_and_dedupe_keyframes(keyframes), bez_chans) @property def _mgr(self):