From ef589175367d3624352102b5eb0e635c85ed41dd Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 18 Feb 2021 18:34:08 -0500 Subject: [PATCH] Refactor keyframe handling for material diffuse anims. The old code was objectively terrible and placed the burden of handling missing values effectively on the user instead of just figuring it out. This is objectively better in that we can now count on all values being "known" at keyframe convert time. Whether that "known" is because it's a real keyframe, we evaluated it, or we pulled it out of our @$$ is another story, of course. --- korman/exporter/animation.py | 567 +++++++++++++++-------------------- 1 file changed, 241 insertions(+), 326 deletions(-) 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):