From ef589175367d3624352102b5eb0e635c85ed41dd Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 18 Feb 2021 18:34:08 -0500 Subject: [PATCH 1/2] 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): From 763b086b9c89af15fb8f16c3712dfb95936fab45 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 18 Feb 2021 19:17:05 -0500 Subject: [PATCH 2/2] Implement material diffuse animations. Closes #188. --- korman/exporter/material.py | 92 +++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 5cc3fa3..06d1f21 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -16,8 +16,10 @@ import bpy import functools import math +import mathutils from pathlib import Path from PyHSPlasma import * +from typing import Union import weakref from .explosions import * @@ -143,7 +145,10 @@ class MaterialConverter: "NONE": self._export_texture_type_none, } self._animation_exporters = { + "ambientCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_ambient), "opacityCtl": self._export_layer_opacity_animation, + "preshadeCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_preshade), + "runtimeCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_runtime), "transformCtl": self._export_layer_transform_animation, } @@ -254,6 +259,7 @@ class MaterialConverter: if not hsgmat.layers: layer = self._mgr.find_create_object(plLayer, name="{}_AutoLayer".format(mat_name), bl=bo) self._propagate_material_settings(bo, bm, layer) + layer = self._export_layer_animations(bo, bm, None, 0, layer) hsgmat.addLayer(layer.key) # Cache this material for later @@ -489,7 +495,7 @@ class MaterialConverter: layer = self._export_layer_animations(bo, bm, slot, idx, layer) return layer - def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer): + def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer) -> plLayer: """Exports animations on this texture and chains the Plasma layers as needed""" def harvest_fcurves(bl_id, collection, data_path=None): @@ -508,9 +514,16 @@ class MaterialConverter: return None fcurves = [] - texture = tex_slot.texture - mat_action = harvest_fcurves(bm, fcurves, "texture_slots[{}]".format(idx)) - tex_action = harvest_fcurves(texture, fcurves) + + # Base layers get all of the fcurves for animating things like the diffuse color + texture = tex_slot.texture if tex_slot is not None else None + if idx == 0: + harvest_fcurves(bm, fcurves) + harvest_fcurves(texture, fcurves) + elif tex_slot is not None: + harvest_fcurves(bm, fcurves, tex_slot.path_from_id()) + harvest_fcurves(texture, fcurves) + if not fcurves: return base_layer @@ -518,7 +531,7 @@ class MaterialConverter: # and chain this biotch up as best we can. layer_animation = None for attr, converter in self._animation_exporters.items(): - ctrl = converter(tex_slot, base_layer, fcurves) + ctrl = converter(bo, bm, tex_slot, base_layer, fcurves) if ctrl is not None: if layer_animation is None: name = "{}_LayerAnim".format(base_layer.key.name) @@ -539,21 +552,39 @@ class MaterialConverter: atc.begin = min((fcurve.range()[0] for fcurve in fcurves)) * (30.0 / fps) / fps atc.end = max((fcurve.range()[1] for fcurve in fcurves)) * (30.0 / fps) / fps - layer_props = tex_slot.texture.plasma_layer - if not layer_props.anim_auto_start: - atc.flags |= plAnimTimeConvert.kStopped - if layer_props.anim_loop: + if tex_slot is not None: + layer_props = tex_slot.texture.plasma_layer + if not layer_props.anim_auto_start: + atc.flags |= plAnimTimeConvert.kStopped + if layer_props.anim_loop: + atc.flags |= plAnimTimeConvert.kLoop + atc.loopBegin = atc.begin + atc.loopEnd = atc.end + if layer_props.anim_sdl_var: + layer_animation.varName = layer_props.anim_sdl_var + else: + # Hmm... I wonder what we should do here? A reasonable default might be to just + # run the stupid thing in a loop. atc.flags |= plAnimTimeConvert.kLoop atc.loopBegin = atc.begin atc.loopEnd = atc.end - if layer_props.anim_sdl_var: - layer_animation.varName = layer_props.anim_sdl_var return layer_animation # Well, we had some FCurves but they were garbage... Too bad. return base_layer - def _export_layer_opacity_animation(self, tex_slot, base_layer, fcurves): + def _export_layer_diffuse_animation(self, bo, bm, tex_slot, base_layer, fcurves, converter): + assert converter is not None + + def translate_color(color_sequence): + # See things like get_material_preshade + result = converter(bo, bm, mathutils.Color(color_sequence)) + return result.red, result.green, result.blue + + ctrl = self._exporter().animation.make_pos_controller(fcurves, "diffuse_color", bm.diffuse_color, translate_color) + return ctrl + + def _export_layer_opacity_animation(self, bo, bm, tex_slot, base_layer, fcurves): for i in fcurves: if i.data_path == "plasma_layer.opacity": base_layer.state.blendFlags |= hsGMatState.kBlendAlpha @@ -561,14 +592,16 @@ class MaterialConverter: return ctrl return None - def _export_layer_transform_animation(self, tex_slot, base_layer, fcurves): - path = tex_slot.path_from_id() - pos_path = "{}.offset".format(path) - scale_path = "{}.scale".format(path) + def _export_layer_transform_animation(self, bo, bm, tex_slot, base_layer, fcurves): + if tex_slot is not None: + path = tex_slot.path_from_id() + pos_path = "{}.offset".format(path) + scale_path = "{}.scale".format(path) - # Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller - ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path, tex_slot.offset, tex_slot.scale) - return ctrl + # Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller + ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path, tex_slot.offset, tex_slot.scale) + return ctrl + return None def _export_texture_type_environment_map(self, bo, layer, slot): """Exports a Blender EnvironmentMapTexture to a plLayer""" @@ -1164,24 +1197,26 @@ class MaterialConverter: def get_bump_layer(self, bo): return self._bump_mats.get(bo, None) - def get_material_ambient(self, bo, bm) -> hsColorRGBA: + def get_material_ambient(self, bo, bm, color : Union[None, mathutils.Color]=None) -> hsColorRGBA: emit_scale = bm.emit * 0.5 if emit_scale > 0.0: - return hsColorRGBA(bm.diffuse_color.r * emit_scale, - bm.diffuse_color.g * emit_scale, - bm.diffuse_color.b * emit_scale, + if color is None: + color = bm.diffuse_color + return hsColorRGBA(color.r * emit_scale, + color.g * emit_scale, + color.b * emit_scale, 1.0) else: return utils.color(bpy.context.scene.world.ambient_color) - def get_material_preshade(self, bo, bm, color=None) -> hsColorRGBA: + def get_material_preshade(self, bo, bm, color : Union[None, mathutils.Color]=None) -> hsColorRGBA: if bo.plasma_modifiers.lighting.rt_lights: return hsColorRGBA.kBlack if color is None: color = bm.diffuse_color return utils.color(color) - def get_material_runtime(self, bo, bm, color=None) -> hsColorRGBA: + def get_material_runtime(self, bo, bm, color : Union[None, mathutils.Color]=None) -> hsColorRGBA: if not bo.plasma_modifiers.lighting.preshade: return hsColorRGBA.kBlack if color is None: @@ -1191,19 +1226,18 @@ class MaterialConverter: def get_texture_animation_key(self, bo, bm, texture): """Finds or creates the appropriate key for sending messages to an animated Texture""" - tex_name = texture.name + tex_name = texture.name if texture is not None else "AutoLayer" if bo.type == "LAMP": assert bm is None bm_name = bo.name else: assert bm is not None bm_name = bm.name - if not tex_name in bm.texture_slots: + if texture is not None and not tex_name in bm.texture_slots: raise ExportError("Texture '{}' not used in Material '{}'".format(bm_name, tex_name)) name = "{}_{}_LayerAnim".format(bm_name, tex_name) - layer = texture.plasma_layer - pClass = plLayerSDLAnimation if layer.anim_sdl_var else plLayerAnimation + pClass = plLayerSDLAnimation if texture is not None and texture.plasma_layer.anim_sdl_var else plLayerAnimation return self._mgr.find_create_key(pClass, bl=bo, name=name) @property